1//////////////////////////////////////////////////////////////////////
2// LibFile: attachments.scad
3// The modules in this file allows you to attach one object to another by making one object the child of another object.
4// You can place the child object in relation to its parent object and control the position and orientation
5// relative to the parent. The modifiers allow you to treat children in ways different from simple union, such
6// as differencing them from the parent, or changing their color. Attachment only works when the parent and child
7// are both written to support attachment. Also included in this file are the tools to make your own "attachable" objects.
8// Includes:
9// include <BOSL2/std.scad>
10// FileGroup: Basic Modeling
11// FileSummary: Positioning objects on or relative to other objects. Making your own objects support attachment.
12// FileFootnotes: STD=Included in std.scad
13//////////////////////////////////////////////////////////////////////
14
15
16// Default values for attachment code.
17$tags=undef; // for backward compatibility
18$tag = "";
19$tag_prefix = "";
20$overlap = 0;
21$color = "default";
22$save_color = undef; // Saved color to revert back for children
23
24$anchor_override = undef;
25$attach_to = undef;
26$attach_anchor = [CENTER, CENTER, UP, 0];
27$attach_alignment = undef;
28
29$parent_anchor = BOTTOM;
30$parent_spin = 0;
31$parent_orient = UP;
32
33$parent_size = undef;
34$parent_geom = undef;
35
36$tags_shown = "ALL";
37$tags_hidden = [];
38
39_ANCHOR_TYPES = ["intersect","hull"];
40
41
42// Section: Terminology and Shortcuts
43// This library adds the concept of anchoring, spin and orientation to the `cube()`, `cylinder()`
44// and `sphere()` builtins, as well as to most of the shapes provided by this library itself.
45// - An anchor is a place on an object which you can align the object to, or attach other objects
46// to using `attach()` or `position()`. An anchor has a position, a direction, and a spin.
47// The direction and spin are used to orient other objects to match when using `attach()`.
48// - Spin is a simple rotation around the Z axis.
49// - Orientation is rotating an object so that its top is pointed towards a given vector.
50// .
51// An object will first be translated to its anchor position, then spun, then oriented.
52// For a detailed step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
53// .
54// For describing directions, faces, edges, and corners the library provides a set of shortcuts
55// all based on combinations of unit direction vectors. You can use these for anchoring and orienting
56// attachable objects. You can also them to specify edge sets for rounding or chamfering cuboids,
57// or for placing edge, face and corner masks.
58// Subsection: Anchor
59// Anchoring is specified with the `anchor` argument in most shape modules. Specifying `anchor`
60// when creating an object will translate the object so that the anchor point is at the origin
61// (0,0,0). Anchoring always occurs before spin and orientation are applied.
62// .
63// An anchor can be referred to in one of two ways; as a directional vector, or as a named anchor string.
64// .
65// When given as a vector, it points, in a general way, towards the face, edge, or corner of the
66// object that you want the anchor for, relative to the center of the object. You can simply
67// specify a vector like `[0,0,1]` to anchor an object at the Z+ end, but you can also use
68// directional constants with names like `TOP`, `BOTTOM`, `LEFT`, `RIGHT` and `BACK` that you can add together
69// to specify anchor points. See [specifying directions](attachments.scad#subsection-specifying-directions)
70// below for the full list of pre-defined directional constants.
71// .
72// For example:
73// - `[0,0,1]` is the same as `TOP` and refers to the center of the top face.
74// - `[-1,0,1]` is the same as `TOP+LEFT`, and refers to the center of the top-left edge.
75// - `[1,1,-1]` is the same as `BOTTOM+BACK+RIGHT`, and refers to the bottom-back-right corner.
76// .
77// When the object is cubical or rectangular in shape the anchors must have zero or one values
78// for their components and they refer to the face centers, edge centers, or corners of the object.
79// The direction of a face anchor will be perpendicular to the face, pointing outward. The direction of a edge anchor
80// will be the average of the anchor directions of the two faces the edge is between. The direction
81// of a corner anchor will be the average of the anchor directions of the three faces the corner is
82// on.
83// .
84// When the object is cylindrical, conical, or spherical in nature, the anchors will be located
85// around the surface of the cylinder, cone, or sphere, relative to the center.
86// You can generally use an arbitrary vector to get an anchor positioned anywhere on the curved
87// surface of such an object, and the anchor direction will be the surface normal at the anchor location.
88// However, for anchor component pointing toward the flat face should be either -1, 1, or 0, and
89// anchors that point diagonally toward one of the flat faces will select a point on the edge.
90// .
91// For objects in two dimensions, the natural expectation is for TOP and BOTTOM to refer to the Y direction
92// of the shape. To support this, if you give an anchor in 2D that has anchor.y=0 then the Z component
93// will be mapped to the Y direction. This means you can use TOP and BOTTOM for anchors of 2D objects.
94// But remember that TOP and BOTTOM are three dimensional vectors and this is a special interpretation
95// for 2d anchoring.
96// .
97// Some more complex objects, like screws and stepper motors, have named anchors to refer to places
98// on the object that are not at one of the standard faces, edges or corners. For example, stepper
99// motors have anchors for `"screw1"`, `"screw2"`, etc. to refer to the various screwholes on the
100// stepper motor shape. The names, positions, directions, and spins of these anchors are
101// specific to the object, and are documented when they exist.
102// Subsection: Spin
103// Spin is specified with the `spin` argument in most shape modules. Specifying a scalar `spin`
104// when creating an object will rotate the object counter-clockwise around the Z axis by the given
105// number of degrees. If given as a 3D vector, the object will be rotated around each of the X, Y, Z
106// axes by the number of degrees in each component of the vector. Spin is always applied after
107// anchoring, and before orientation. Since spin is applied after anchoring it is not what
108// you might think of intuitively as spinning the shape. To do that, apply `zrot()` to the shape before anchoring.
109// Subsection: Orient
110// Orientation is specified with the `orient` argument in most shape modules. Specifying `orient`
111// when creating an object will rotate the object such that the top of the object will be pointed
112// at the vector direction given in the `orient` argument. Orientation is always applied after
113// anchoring and spin. The constants `UP`, `DOWN`, `FRONT`, `BACK`, `LEFT`, and `RIGHT` can be
114// added together to form the directional vector for this. ie: `LEFT+BACK`
115// Subsection: Specifying Directions
116// You can use direction vectors to specify anchors for objects or to specify edges, faces, and
117// corners of cubes. You can simply specify these direction vectors numerically, but another
118// option is to use named constants for direction vectors. These constants define unit vectors
119// for the six axis directions as shown below.
120// Figure(3D,Big,VPD=6): Named constants for direction vectors. Some directions have more than one name.
121// $fn=12;
122// stroke([[0,0,0],RIGHT], endcap2="arrow2", width=.05);
123// color("black")right(.05)up(.05)move(RIGHT) text3d("RIGHT",size=.1,h=.01,anchor=LEFT,orient=FRONT);
124// stroke([[0,0,0],LEFT], endcap2="arrow2", width=.05);
125// color("black")left(.05)up(.05)move(LEFT) text3d("LEFT",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
126// stroke([[0,0,0],FRONT], endcap2="arrow2", width=.05);
127// color("black")
128// left(.1){
129// up(.12)move(FRONT) text3d("FRONT",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
130// move(FRONT) text3d("FWD",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
131// down(.12)move(FRONT) text3d("FORWARD",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
132// }
133// stroke([[0,0,0],BACK], endcap2="arrow2", width=.05);
134// right(.05)
135// color("black")move(BACK) text3d("BACK",size=.1,h=.01,anchor=LEFT,orient=FRONT);
136// stroke([[0,0,0],DOWN], endcap2="arrow2", width=.05);
137// color("black")
138// right(.1){
139// up(.12)move(BOT) text3d("DOWN",size=.1,h=.01,anchor=LEFT,orient=FRONT);
140// move(BOT) text3d("BOTTOM",size=.1,h=.01,anchor=LEFT,orient=FRONT);
141// down(.12)move(BOT) text3d("BOT",size=.1,h=.01,anchor=LEFT,orient=FRONT);
142// }
143// stroke([[0,0,0],TOP], endcap2="arrow2", width=.05);
144// color("black")left(.05){
145// up(.12)move(TOP) text3d("TOP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
146// move(TOP) text3d("UP",size=.1,h=.01,anchor=RIGHT,orient=FRONT);
147// }
148// Figure(2D,Big): Named constants for direction vectors in 2D. For anchors the TOP and BOTTOM directions are collapsed into 2D as shown here, but do not try to use TOP or BOTTOM as 2D directions in other situations.
149// $fn=12;
150// stroke(path2d([[0,0,0],RIGHT]), endcap2="arrow2", width=.05);
151// color("black")fwd(.22)left(.05)move(RIGHT) text("RIGHT",size=.1,anchor=RIGHT);
152// stroke(path2d([[0,0,0],LEFT]), endcap2="arrow2", width=.05);
153// color("black")right(.05)fwd(.22)move(LEFT) text("LEFT",size=.1,anchor=LEFT);
154// stroke(path2d([[0,0,0],FRONT]), endcap2="arrow2", width=.05);
155// color("black")
156// fwd(.2)
157// right(.15)
158// color("black")move(BACK) { text("BACK",size=.1,anchor=LEFT); back(.14) text("(TOP)", size=.1, anchor=LEFT);}
159// color("black")
160// left(.15)back(.2+.14)move(FRONT){
161// back(.14) text("FRONT",size=.1,anchor=RIGHT);
162// text("FWD",size=.1,anchor=RIGHT);
163// fwd(.14) text("FORWARD",size=.1,anchor=RIGHT);
164// fwd(.28) text("(BOTTOM)",size=.1,anchor=RIGHT);
165// fwd(.14*3) text("(BOT)",size=.1,anchor=RIGHT);
166// }
167// stroke(path2d([[0,0,0],BACK]), endcap2="arrow2", width=.05);
168// Subsection: Specifying Faces
169// Modules operating on faces accept a list of faces to describe the faces to operate on. Each
170// face is given by a vector that points to that face. Attachments of cuboid objects onto their faces also
171// work by choosing an attachment face with a single vector in the same manner.
172// Figure(3D,Big,NoScales,VPD=275): The six faces of the cube. Some have faces have more than one name.
173// ydistribute(50) {
174// xdistribute(35){
175// _show_cube_faces([BACK], botlabel=["BACK"]);
176// _show_cube_faces([UP],botlabel=["TOP","UP"]);
177// _show_cube_faces([RIGHT],botlabel=["RIGHT"]);
178// }
179// xdistribute(35){
180// _show_cube_faces([FRONT],toplabel=["FRONT","FWD", "FORWARD"]);
181// _show_cube_faces([DOWN],toplabel=["BOTTOM","BOT","DOWN"]);
182// _show_cube_faces([LEFT],toplabel=["LEFT"]);
183// }
184// }
185// Subsection: Specifying Edges
186// Modules operating on edges use two arguments to describe the edge set they will use: The `edges` argument
187// is a list of edge set descriptors to include in the edge set, and the `except` argument is a list of
188// edge set descriptors to remove from the edge set.
189// The default value for `edges` is `"ALL"`, the set of all edges.
190// The default value for `except` is the empty set, meaning no edges are removed.
191// If either argument is just a single edge set
192// descriptor it can be passed directly rather than in a singleton list.
193// Each edge set descriptor must be one of:
194// - A vector pointing towards an edge, indicating that single edge.
195// - A vector pointing towards a face, indicating all edges surrounding that face.
196// - A vector pointing towards a corner, indicating all edges touching that corner.
197// - The string `"X"`, indicating all X axis aligned edges.
198// - The string `"Y"`, indicating all Y axis aligned edges.
199// - The string `"Z"`, indicating all Z axis aligned edges.
200// - The string `"ALL"`, indicating all edges.
201// - The string `"NONE"`, indicating no edges at all.
202// - A 3x4 array, where each entry corresponds to one of the 12 edges and is set to 1 if that edge is included and 0 if the edge is not. The edge ordering is:
203// ```
204// [
205// [Y-Z-, Y+Z-, Y-Z+, Y+Z+],
206// [X-Z-, X+Z-, X-Z+, X+Z+],
207// [X-Y-, X+Y-, X-Y+, X+Y+]
208// ]
209// ```
210// .
211// You can specify edge descriptors directly by giving a vector, or you can use sums of the
212// named direction vectors described above. Below we show all of the edge sets you can
213// describe with sums of the direction vectors, and then we show some examples of combining
214// edge set descriptors.
215// Figure(3D,Big,VPD=300,NoScales): Vectors pointing toward an edge select that single edge
216// ydistribute(50) {
217// xdistribute(30) {
218// _show_edges(edges=BOT+RIGHT);
219// _show_edges(edges=BOT+BACK);
220// _show_edges(edges=BOT+LEFT);
221// _show_edges(edges=BOT+FRONT);
222// }
223// xdistribute(30) {
224// _show_edges(edges=FWD+RIGHT);
225// _show_edges(edges=BACK+RIGHT);
226// _show_edges(edges=BACK+LEFT);
227// _show_edges(edges=FWD+LEFT);
228// }
229// xdistribute(30) {
230// _show_edges(edges=TOP+RIGHT);
231// _show_edges(edges=TOP+BACK);
232// _show_edges(edges=TOP+LEFT);
233// _show_edges(edges=TOP+FRONT);
234// }
235// }
236// Figure(3D,Med,VPD=205,NoScales): Vectors pointing toward a face select all edges surrounding that face.
237// ydistribute(50) {
238// xdistribute(30) {
239// _show_edges(edges=LEFT);
240// _show_edges(edges=FRONT);
241// _show_edges(edges=RIGHT);
242// }
243// xdistribute(30) {
244// _show_edges(edges=TOP);
245// _show_edges(edges=BACK);
246// _show_edges(edges=BOTTOM);
247// }
248// }
249// Figure(3D,Big,VPD=300,NoScales): Vectors pointing toward a corner select all edges surrounding that corner.
250// ydistribute(50) {
251// xdistribute(30) {
252// _show_edges(edges=FRONT+LEFT+TOP);
253// _show_edges(edges=FRONT+RIGHT+TOP);
254// _show_edges(edges=FRONT+LEFT+BOT);
255// _show_edges(edges=FRONT+RIGHT+BOT);
256// }
257// xdistribute(30) {
258// _show_edges(edges=TOP+LEFT+BACK);
259// _show_edges(edges=TOP+RIGHT+BACK);
260// _show_edges(edges=BOT+LEFT+BACK);
261// _show_edges(edges=BOT+RIGHT+BACK);
262// }
263// }
264// Figure(3D,Med,VPD=205,NoScales): Named Edge Sets
265// ydistribute(50) {
266// xdistribute(30) {
267// _show_edges(edges="X");
268// _show_edges(edges="Y");
269// _show_edges(edges="Z");
270// }
271// xdistribute(30) {
272// _show_edges(edges="ALL");
273// _show_edges(edges="NONE");
274// }
275// }
276// Figure(3D,Big,VPD=310,NoScales): Next are some examples showing how you can combine edge descriptors to obtain different edge sets. You can specify the top front edge with a numerical vector or by combining the named direction vectors. If you combine them as a list you get all the edges around the front and top faces. Adding `except` removes an edge.
277// xdistribute(43){
278// _show_edges(_edges([0,-1,1]),toplabel=["edges=[0,-1,1]"]);
279// _show_edges(_edges(TOP+FRONT),toplabel=["edges=TOP+FRONT"]);
280// _show_edges(_edges([TOP,FRONT]),toplabel=["edges=[TOP,FRONT]"]);
281// _show_edges(_edges([TOP,FRONT],TOP+FRONT),toplabel=["edges=[TOP,FRONT]","except=TOP+FRONT"]);
282// }
283// Figure(3D,Big,VPD=310,NoScales): Using `except=BACK` removes the four edges surrounding the back face if they are present in the edge set. In the first example only one edge needs to be removed. In the second example we remove two of the Z-aligned edges. The third example removes all four back edges from the default edge set of all edges. You can explicitly give `edges="ALL"` but it is not necessary, since this is the default. In the fourth example, the edge set of Y-aligned edges contains no back edges, so the `except` parameter has no effect.
284// xdistribute(43){
285// _show_edges(_edges(BOT,BACK), toplabel=["edges=BOT","except=BACK"]);
286// _show_edges(_edges("Z",BACK), toplabel=["edges=\"Z\"", "except=BACK"]);
287// _show_edges(_edges("ALL",BACK), toplabel=["(edges=\"ALL\")", "except=BACK"]);
288// _show_edges(_edges("Y",BACK), toplabel=["edges=\"Y\"","except=BACK"]);
289// }
290// Figure(3D,Big,NoScales,VPD=310): On the left `except` is a list to remove two edges. In the center we show a corner edge set defined by a numerical vector, and at the right we remove that same corner edge set with named direction vectors.
291// xdistribute(52){
292// _show_edges(_edges("ALL",[FRONT+RIGHT,FRONT+LEFT]),
293// toplabel=["except=[FRONT+RIGHT,"," FRONT+LEFT]"]);
294// _show_edges(_edges([1,-1,1]),toplabel=["edges=[1,-1,1]"]);
295// _show_edges(_edges([TOP,BOT], TOP+RIGHT+FRONT),toplabel=["edges=[TOP,BOT]","except=TOP+RIGHT+FRONT"]);
296// }
297// Subsection: Specifying Corners
298// Modules operating on corners use two arguments to describe the corner set they will use: The `corners` argument
299// is a list of corner set descriptors to include in the corner set, and the `except` argument is a list of
300// corner set descriptors to remove from the corner set.
301// The default value for `corners` is `"ALL"`, the set of all corners.
302// The default value for `except` is the empty set, meaning no corners are removed.
303// If either argument is just a single corner set
304// descriptor it can be passed directly rather than in a singleton list.
305// Each corner set descriptor must be one of:
306// - A vector pointing towards a corner, indicating that corner.
307// - A vector pointing towards an edge indicating both corners at the ends of that edge.
308// - A vector pointing towards a face, indicating all the corners of that face.
309// - The string `"ALL"`, indicating all corners.
310// - The string `"NONE"`, indicating no corners at all.
311// - A length 8 vector where each entry corresponds to a corner and is 1 if the corner is included and 0 if it is excluded. The corner ordering is
312// ```
313// [X-Y-Z-, X+Y-Z-, X-Y+Z-, X+Y+Z-, X-Y-Z+, X+Y-Z+, X-Y+Z+, X+Y+Z+]
314// ```
315// .
316// You can specify corner descriptors directly by giving a vector, or you can use sums of the
317// named direction vectors described above. Below we show all of the corner sets you can
318// describe with sums of the direction vectors and then we show some examples of combining
319// corner set descriptors.
320// Figure(3D,Big,NoScales,VPD=300): Vectors pointing toward a corner select that corner.
321// ydistribute(55) {
322// xdistribute(35) {
323// _show_corners(corners=FRONT+LEFT+TOP);
324// _show_corners(corners=FRONT+RIGHT+TOP);
325// _show_corners(corners=FRONT+LEFT+BOT);
326// _show_corners(corners=FRONT+RIGHT+BOT);
327// }
328// xdistribute(35) {
329// _show_corners(corners=TOP+LEFT+BACK);
330// _show_corners(corners=TOP+RIGHT+BACK);
331// _show_corners(corners=BOT+LEFT+BACK);
332// _show_corners(corners=BOT+RIGHT+BACK);
333// }
334// }
335// Figure(3D,Big,NoScales,VPD=340): Vectors pointing toward an edge select the corners and the ends of the edge.
336// ydistribute(55) {
337// xdistribute(35) {
338// _show_corners(corners=BOT+RIGHT);
339// _show_corners(corners=BOT+BACK);
340// _show_corners(corners=BOT+LEFT);
341// _show_corners(corners=BOT+FRONT);
342// }
343// xdistribute(35) {
344// _show_corners(corners=FWD+RIGHT);
345// _show_corners(corners=BACK+RIGHT);
346// _show_corners(corners=BACK+LEFT);
347// _show_corners(corners=FWD+LEFT);
348// }
349// xdistribute(35) {
350// _show_corners(corners=TOP+RIGHT);
351// _show_corners(corners=TOP+BACK);
352// _show_corners(corners=TOP+LEFT);
353// _show_corners(corners=TOP+FRONT);
354// }
355// }
356// Figure(3D,Med,NoScales,VPD=225): Vectors pointing toward a face select the corners of the face.
357// ydistribute(55) {
358// xdistribute(35) {
359// _show_corners(corners=LEFT);
360// _show_corners(corners=FRONT);
361// _show_corners(corners=RIGHT);
362// }
363// xdistribute(35) {
364// _show_corners(corners=TOP);
365// _show_corners(corners=BACK);
366// _show_corners(corners=BOTTOM);
367// }
368// }
369// Figure(3D,Med,NoScales,VPD=200): Corners by name
370// xdistribute(35) {
371// _show_corners(corners="ALL");
372// _show_corners(corners="NONE");
373// }
374// Figure(3D,Big,NoScales,VPD=300): Next are some examples showing how you can combine corner descriptors to obtain different corner sets. You can specify corner sets numerically or by adding together named directions. The third example shows a list of two corner specifications, giving all the corners on the front face or the right face.
375// xdistribute(52){
376// _show_corners(_corners([1,-1,-1]),toplabel=["corners=[1,-1,-1]"]);
377// _show_corners(_corners(BOT+RIGHT+FRONT),toplabel=["corners=BOT+RIGHT+FRONT"]);
378// _show_corners(_corners([FRONT,RIGHT]), toplabel=["corners=[FRONT,RIGHT]"]);
379// }
380// Figure(3D,Big,NoScales,VPD=300): Corners for one edge, two edges, and all the edges except the two on one edge. Note that since the default is all edges, you only need to give the except argument in this case:
381// xdistribute(52){
382// _show_corners(_corners(FRONT+TOP), toplabel=["corners=FRONT+TOP"]);
383// _show_corners(_corners([FRONT+TOP,BOT+BACK]), toplabel=["corners=[FRONT+TOP,"," BOT+BACK]"]);
384// _show_corners(_corners("ALL",FRONT+TOP), toplabel=["(corners=\"ALL\")","except=FRONT+TOP"]);
385// }
386// Figure(3D,Med,NoScales,VPD=240): The first example shows a single corner removed from the top corners using a numerical vector. The second one shows removing a set of two corner descriptors from the implied set of all corners.
387// xdistribute(58){
388// _show_corners(_corners(TOP,[1,1,1]), toplabel=["corners=TOP","except=[1,1,1]"]);
389// _show_corners(_corners("ALL",[FRONT+RIGHT+TOP,FRONT+LEFT+BOT]),
390// toplabel=["except=[FRONT+RIGHT+TOP,"," FRONT+LEFT+BOT]"]);
391// }
392// Subsection: Anchoring of Non-Rectangular Objects and Anchor Type (atype)
393// We focused above on rectangular objects that have well-defined faces and edges aligned with the coordinate axes.
394// Things get difficult when the objects are curved, or even when their edges are not neatly aligned with the coordinate axes.
395// In these cases, the library may provide multiple different anchoring schemes, called the anchor types. When a module supports
396// multiple anchor types, use the `atype=` parameter to select the anchor type you need.
397// .
398// First consider the case of a simple rectangle whose corners have been rounded. Where should the anchors lie?
399// The default anchor type puts them in the same location as the anchors of an unrounded rectangle, which means that for
400// positive rounding radii, they are not even located on the perimeter of the object.
401// Figure(2D,Med,NoAxes): Default "box" atype anchors for a rounded {{rect()}}
402// rect([100,50], rounding=[10,0,0,-20],chamfer=[0,10,-20,0]) show_anchors();
403// Continues:
404// This choice enables you to position the box, or attach things to it, without regard to its rounding or chamfers. If you need to
405// anchor onto the roundovers or chamfers then you can use the "perim" anchor type:
406// Figure(2D,Med,NoAxes): The "perim" atype for a rounded and chamfered {{rect()}}
407// rect([100,50], rounding=[10,0,0,-20],chamfer=[0,10,-20,0],atype="perim") show_anchors();
408// Continues:
409// With this anchor type, the anchors are located on the perimeter. For positive roundings they point in the standard anchor direction;
410// for negative roundings they are parallel to the base. As noted above, for circles, cylinders, and spheres, the anchor point is
411// determined by choosing the point where the anchor vector intersects the shape. On a circle, this results in an anchor whose direction
412// matches the user provided anchor vector. But on an ellipse, something else happens:
413// Figure(2D,Med,NoAxes): Anchors on an ellipse. The red arrow shows a TOP+RIGHT anchor direction.
414// ellipse([70,30]) show_anchors();
415// stroke([[0,0],[45,45]], color="red",endcap2="arrow2");
416// Continues:
417// For a TOP+RIGHT anchor direction, the surface normal at the intersection point does not match the anchor direction,
418// so the direction of the anchor shown in blue does not match the direction specified, in red.
419// Anchors computed this way have anchor type "intersect". When a shape is concave, intersection anchors can produce
420// a result buried inside the shape's concavity. Consider the RIGHT anchor of this supershape example:
421// Figure(2D,Med,NoAxes): A supershape with "intersect" anchor type:
422// supershape(n=150,r=75, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="intersect") show_anchors();
423// Continues:
424// A different anchor type called "hull" finds anchors that are on the convex hull of the shape.
425// Figure(2D,Med,NoAxes): A supershape with "hull" anchor type:
426// supershape(n=150,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="hull") show_anchors();
427// Continues:
428// Hull anchoring works by creating the line (or plane in 3D) that is normal to the specified anchor direction, and
429// finding the point farthest from the center that intersects that line (or plane).
430// Figure(2D,Med,NoAxes): Finding the RIGHT and BACK+LEFT "hull" anchors
431// supershape(n=128,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9,atype="hull") {
432// position(RIGHT) color_this("red")rect([1,90],anchor=LEFT);
433// attach(RIGHT)anchor_arrow2d(13);
434// attach(BACK+LEFT) {
435// anchor_arrow2d(13);
436// color_this("red")rect([30,1]);
437// }
438// }
439// Continues:
440// In the example the RIGHT anchor is found when the normal line (shown in red) is tangent to the shape at two points.
441// The anchor is then taken to be the midpoint. The BACK+LEFT anchor occurs with a single tangent point, and the
442// anchor point is located at the tangent point. For circles intersection is done to the exact circle, but for other
443// shapes these calculations are done on the point lists that defines the shape, so if you change the number of points
444// in the list, the precise location of the anchors can change. You can also get surprising results if your point list is badly chosen.
445// Figure(2D,Med,NoAxes): Circle anchor in blue. The red anchor is computed to a point list of a circle with 17 segments.
446// circle(r=31,$fn=128) attach(TOP)anchor_arrow2d(15);
447// region(circle(r=33,$fn=17)) {color("red")attach(TOP)anchor_arrow2d(13);}
448// Continues:
449// The figure shows a large horizontal offset due to a poor choice of sampling for the circular shape when using the "hull" anchor type.
450// The determination of "hull" or "intersect" anchors may depend on the location of the centerpoint used in the computation.
451// Some of the modules allow you to change the centerpoint using a `cp=` argument. If you need to change the centerpoint for
452// a module that does not provide this option, you can use the generic {{region()}} module, which will let you specify a centerpoint.
453// The default center point is the centroid, specified by "centroid". You can also choose "mean", which gives the mean of all
454// the data points, or "bbox", which gives the centerpoint of the bounding box for the data. Your last option for centerpoint is to
455// choose an arbitrary point that meets your needs.
456// Figure(2D,Med,NoAxes): The centerpoint for "intersect" anchors is located at the red dot
457// region(supershape(n=128,r=55, m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9),atype="intersect",cp=[0,30]) show_anchors();
458// color("red")back(30)circle(r=2,$fn=16);
459// Continues:
460// Note that all the anchors for an object have to be determined based on one anchor type and relative to the same centerpoint.
461// The supported anchor types for each module appear in the "Anchor Types" section of its entry.
462
463
464
465
466
467// Section: Attachment Positioning
468
469// Module: position()
470// Synopsis: Attaches children to a parent object at an anchor point.
471// SynTags: Trans
472// Topics: Attachments
473// See Also: attachable(), attach(), orient()
474// Usage:
475// PARENT() position(at) CHILDREN;
476// Description:
477// Attaches children to a parent object at an anchor point. For a step-by-step explanation
478// of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
479// Arguments:
480// at = The vector, or name of the parent anchor point to attach to.
481// Side Effects:
482// `$attach_anchor` for each `from=` anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
483// `$attach_to` is set to `undef`.
484// Example:
485// spheroid(d=20) {
486// position(TOP) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
487// position(RIGHT) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
488// position(FRONT) cyl(l=10, d1=10, d2=5, anchor=BOTTOM);
489// }
490module position(at,from)
491{
492 if (is_def(from)){
493 echo("'from' argument of position() has changed to 'at' and will be removed in a future version");
494 }
495 dummy0=assert(num_defined([at,from])==1, "Cannot give both `at` argument and the deprectated `from` argument to position()");
496 at = first_defined([at,from]);
497 req_children($children);
498 dummy1=assert($parent_geom != undef, "No object to position relative to.");
499 anchors = (is_vector(at)||is_string(at))? [at] : at;
500 two_d = _attach_geom_2d($parent_geom);
501 for (anchr = anchors) {
502 anch = _find_anchor(anchr, $parent_geom);
503 $attach_to = undef;
504 $attach_anchor = anch;
505 translate(anch[1]) children();
506 }
507}
508
509
510
511// Module: orient()
512// Synopsis: Orients children's tops in the directon of the specified anchor.
513// SynTags: Trans
514// Topics: Attachments
515// See Also: attachable(), attach(), position()
516// Usage:
517// PARENT() orient(anchor, [spin]) CHILDREN;
518// Description:
519// Orients children such that their top is tilted in the direction of the specified parent anchor point.
520// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
521// Arguments:
522// anchor = The anchor on the parent which you want to match the orientation of.
523// spin = The spin to add to the children. (Overrides anchor spin.)
524// Side Effects:
525// `$attach_to` is set to `undef`.
526// Example: When orienting to an anchor, the spin of the anchor may cause confusion:
527// prismoid([50,50],[30,30],h=40) {
528// position(TOP+RIGHT)
529// orient(RIGHT)
530// prismoid([30,30],[0,5],h=20,anchor=BOT+LEFT);
531// }
532// Example: You can override anchor spin with `spin=`.
533// prismoid([50,50],[30,30],h=40) {
534// position(TOP+RIGHT)
535// orient(RIGHT,spin=0)
536// prismoid([30,30],[0,5],h=20,anchor=BOT+LEFT);
537// }
538// Example: Or you can anchor the child from the back
539// prismoid([50,50],[30,30],h=40) {
540// position(TOP+RIGHT)
541// orient(RIGHT)
542// prismoid([30,30],[0,5],h=20,anchor=BOT+BACK);
543// }
544module orient(anchor, spin) {
545 req_children($children);
546 check=
547 assert($parent_geom != undef, "No parent to orient from!")
548 assert(is_string(anchor) || is_vector(anchor));
549 anch = _find_anchor(anchor, $parent_geom);
550 two_d = _attach_geom_2d($parent_geom);
551 fromvec = two_d? BACK : UP;
552 spin = default(spin, anch[3]);
553 dummy=assert(is_finite(spin));
554
555 $attach_to = undef;
556 if (two_d)
557 rot(spin)rot(from=fromvec, to=anch[2]) children();
558 else
559 rot(spin, from=fromvec, to=anch[2]) children();
560}
561
562
563// Module: align()
564// Synopsis: Position children with alignment to parent edges.
565// SynTags: Trans
566// Topics: Attachments
567// See Also: attachable(), attach(), position(), orient()
568// Usage:
569// PARENT() align(anchor, [align], [inside=], [inset=], [shiftout=], [overlap=]) CHILDREN;
570// Description:
571// Place a child on the face identified by `anchor`. If align is not given or is CENTER
572// then the child will be centered on top of the specified face, outside the parent object. The align parameter is a
573// direction defining an edge or corner to align to. The child will be aligned to that edge or corner by
574// choosing an appropriate anchor on the child.
575// Like {{position()}} this module never rotates the child. If you give `anchor=RIGHT` then the child
576// will be given the LEFT anchor and placed adjacent to the parent. You can use `orient=` or `spin=`
577// with the child and the alignment will adjust to select the correct child anchor. Note that if
578// you spin the child by an amount not a multiple of 90 degrees then an edge of the child will be
579// placed against the parent. This module makes it easy to place children aligned flush with the edges
580// of the parent, even after orienting them or spinning them. In contrast {{position()}} can
581// do the same thing but you would have to figure out the correct child anchor, which is not always obvious.
582// .
583// Because `align()` works by setting the child anchor, it overrides any anchor you specify to the child:
584// **any `anchor=` value given to the child is ignored.**
585// .
586// Several options can adjust how the child is positioned. You can specify `inset=` to inset the
587// aligned object from its alignment location. If you set `inside=true` then the
588// child will appear inside the parent instead of on its surface so that you can use {{diff()}} to subract it.
589// In this case the child recieved a default "remove" tag. The `shiftout=` option works with `inside=true` to
590// shift the child out by the specified distance so that the child doesn't exactly align with the parent.
591// .
592// Note that in the description above the anchor was said to define a "face". You can also use this module
593// with an edge anchor, in which case a corner of the child will be placed in contact with the specified
594// edge and the align direction will shift the child to either end of the edge. You can even give a
595// corner as the anchor point, but in that case the only allowed alignment is CENTER.
596// .
597// If you give a list of anchors and/or a list of align directions then all combinations are generated.
598// In this way align() acts like a distributor, creating multiple copies of the child.
599// Named anchors are not supported by `align()`.
600// Arguments:
601// anchor = parent anchor or list of parent anchors for positioning children.
602// align = optional alignment direction or directions for aligning the children. Default: CENTER
603// ---
604// inside = if true, place object inside the parent instead of outside. Default: false
605// inset = shift the child away from the alignment edge/corner by this amount. Default: 0
606// shiftout = Shift an inside object outward so that it overlaps all the aligned faces. Default: 0
607// overlap = Amount to sink the child into the parent. Defaults to `$overlap` which is zero by default.
608// Side Effects:
609// `$anchor` set to the anchor value used for the child.
610// `$align` set to the align value used for the child.
611// `$idx` set to a unique index for each child, increasing by alignment first.
612// `$attach_anchor` for each anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
613// if inside is true then set default tag to "remove"
614// Example: Cuboid positioned on the right of its parent. Note that it is in its native orientation.
615// cuboid([20,35,25])
616// align(RIGHT)
617// color("lightgreen")cuboid([5,1,9]);
618// Example: Child would require anchor of RIGHT+FRONT+BOT if placed with {{position()}}.
619// cuboid([50,40,15])
620// align(TOP,RIGHT+FRONT)
621// color("lightblue")prismoid([10,5],[7,4],height=4);
622// Example: Child requires a different anchor for each position, so a simple explicit specification of the anchor for children is impossible in this case, without using two separate commands.
623// cuboid([50,40,15])
624// align(TOP,[RIGHT,LEFT])
625// color("lightblue")prismoid([10,5],[7,4],height=4);
626// Example: If you spin the child 90 deg it is still flush with the edge of the parent. In this case the required anchor for the child is BOT+FWD:
627// cuboid([50,40,15])
628// align(TOP,RIGHT)
629// color("lightblue")
630// prismoid([10,5],[7,4],height=4,spin=90);
631// Example: Here the child is placed on the RIGHT face. Notice how the TOP+LEFT anchor of the prismoid is aligned with the edge of the parent. The prismoid remains in the same orientation.
632// cuboid([50,40,15])
633// align(RIGHT,TOP)
634// color("lightblue")prismoid([10,5],[7,4],height=4);
635// Example: If you change the orientation of the child it still appears aligned flush in its changed orientation:
636// cuboid([50,40,15])
637// align(TOP, RIGHT)
638// color("lightblue")prismoid([10,5],[7,4],height=4,orient=DOWN);
639// Example: The center of the cubes edge is lined up with the center of the prismoid edge, so this result is the expected result:
640// prismoid(50,30,25)
641// align(RIGHT,FRONT)
642// color("lightblue")cuboid(8);
643// Example: Spinning the cube means that the corner of the cube is the most extreme point, so that's what aligns with the front edge of the parent:
644// cuboid([50,40,15])
645// align(TOP,FWD)
646// color("lightblue")cuboid(9,spin=22);
647// Example: A similar thing happens if you attach a cube to a cylinder with an arbitrary anchor angle:
648// cyl(h=20,d=10,$fn=128)
649// align([1,.3],TOP)
650// color("lightblue")cuboid(5);
651// Example: Orienting the child is done in the global coordinate system (as usual) not in the parent coordinate system. Note that the blue prismoid is not lined up with the parent face. (To place the child on the face use {{attach()}}.
652// prismoid(50,30,25)
653// align(RIGHT)
654// color("lightblue")prismoid([10,5],[7,4],height=4,orient=RIGHT);
655// Example: Setting `inside=true` enables us to subtract the child from the parent with {{diff()}}. The "remove" tag is automatically applied when you set `inside=true`, and we used `shiftout=0.01` to prevent z-fighting on the faces.
656// diff()
657// cuboid([40,30,10])
658// align(FRONT,TOP,inside=true,shiftout=0.01)
659// prismoid([10,5],[7,5],height=4);
660// Example: Setting inset shifts all of the children away from their aligned edge, which is a different direction for each child.
661// cuboid([40,30,30])
662// align(FRONT,[TOP,BOT,LEFT,RIGHT,TOP+RIGHT,BOT+LEFT], inset=3)
663// color("green") cuboid(2);
664// Example: Changing the child characteristics based on the alignment
665// cuboid([20,20,8])
666// align(TOP,[for(i=[-1:1], j=[-1:1]) [i,j]])
667// color("orange")
668// if (norm($align)==0) cuboid([3,3,1]);
669// else if (norm($align)==norm([1,1])) cuboid([3,3,4.5]);
670// else cuboid(3);
671// Example: In this example the pink cubes are positioned onto an edge. They meet edge-to-edge. Aligning left shifts the cube to the left end of the edge.
672// cuboid([30,30,20])
673// align(TOP+BACK,[CTR,LEFT])
674// color("pink")cuboid(4);
675// Example: Normally `overlap` is used to create a tiny overlap to keep CGAL happy, but you can also give it a large value as shown here:
676// cuboid([30,30,20])
677// align(TOP+BACK,[RIGHT,CTR,LEFT],overlap=2)
678// color("lightblue")cuboid(4);
679
680module align(anchor,align=CENTER,inside=false,inset=0,shiftout=0,overlap)
681{
682 req_children($children);
683 overlap = (overlap!=undef)? overlap : $overlap;
684 dummy1=assert($parent_geom != undef, "No object to align to.")
685 assert(is_undef($attach_to), "Cannot use align() as a child of attach()");
686 if (is_undef($align_msg) || $align_msg)
687 echo("ALERT: align() has changed, May 1, 2024. See the wiki and attach(align=). $align_msg=false disables this message");
688 anchor = is_vector(anchor) ? [anchor] : anchor;
689 align = is_vector(align) ? [align] : align;
690 two_d = _attach_geom_2d($parent_geom);
691 factor = inside?-1:1;
692 for (i = idx(anchor)) {
693 $align_msg=false; // Remove me when removing the message above
694 face = anchor[i];
695 $anchor=face;
696 dummy=
697 assert(!is_string(face),
698 str("Named anchor \"",face,"\" given for anchor, but align() does not support named anchors"))
699 assert(is_vector(face) && (len(face)==2 || len(face)==3),
700 str("Invalid face ",face, ". Must be a 2-vector or 3-vector"));
701 thisface = two_d? _force_anchor_2d(face) : point3d(face);
702 for(j = idx(align)) {
703 edge=align[j];
704 $idx = j+len(align)*i;
705 $align=edge;
706 dummy1=assert(is_vector(edge) && (len(edge)==2 || len(edge)==3),
707 "align direction must be a 2-vector or 3-vector");
708 thisedge = two_d? _force_anchor_2d(edge) : point3d(edge);
709 dummy=assert(all_zero(v_mul(thisedge,thisface)),
710 str("align (",thisedge,") cannot include component parallel to anchor ",thisface));
711 thisface_anch = _find_anchor(thisface, $parent_geom);
712 inset_dir = two_d ? -thisface
713 : unit(thisface_anch[1]-_find_anchor([thisedge.x,0,0]+thisface, $parent_geom)[1],CTR)
714 +unit(thisface_anch[1]-_find_anchor([0,thisedge.y,0]+thisface, $parent_geom)[1],CTR)
715 +unit(thisface_anch[1]-_find_anchor([0,0,thisedge.z]+thisface, $parent_geom)[1],CTR);
716
717 pos_anch = _find_anchor(thisface+thisedge, $parent_geom);
718 $attach_alignment = thisedge-factor*thisface;
719 $attach_anchor=list_set(pos_anch,2,UP);
720 translate(pos_anch[1]
721 +inset*inset_dir
722 +shiftout*(thisface_anch[2]-inset_dir)
723 -overlap*thisface_anch[2])
724 default_tag("remove",inside) children();
725 }
726 }
727}
728
729// Quantize anchor entry to {-1,0,1}
730function _quant_anch(x) = approx(x,0) ? 0 : sign(x);
731
732// Make arbitrary anchor legal for a given geometry
733function _make_anchor_legal(anchor,geom) =
734 in_list(geom[0], ["prismoid","trapezoid"]) ? [for(v=anchor) _quant_anch(v)]
735 : in_list(geom[0], ["conoid", "extrusion_extent"]) ? [anchor.x,anchor.y, _quant_anch(anchor.z)]
736 : anchor;
737
738
739
740// Module: attach()
741// Synopsis: Attaches children to a parent object at an anchor point and with anchor orientation.
742// SynTags: Trans
743// Topics: Attachments
744// See Also: attachable(), position(), align(), face_profile(), edge_profile(), corner_profile()
745// Usage:
746// PARENT() attach(parent, child, [align=], [spin=], [overlap=], [inside=], [inset=], [shiftout=]) CHILDREN;
747// PARENT() attach(parent, [overlap=], [spin=]) CHILDREN;
748// Description:
749// Attaches children to a parent object at an anchor point or points, oriented in the anchor direction.
750// This module differs from {{position()}} and {{align()}} in that it rotates the children to
751// the anchor direction, which generally means it places the children on the surface of a parent.
752// There are two modes of operation, parent anchor (single argument) and parent-child anchor (double argument).
753// .
754// The parent-child anchor (double argument) version is usually easier to use, and it is more powerful because it supports
755// alignment. You provide an anchor on the parent (`parent`) and an anchor on the child (`child`).
756// This module connects the `child` anchor on the child to the `parent` anchor on the parent.
757// Imagine pointing the parent and child anchor arrows at each other and pushing the objects
758// together until they meet at the anchor point. The most basic case
759// is `attach(TOP,BOT)` which puts the bottom of the child onto the top of the parent. If you
760// do `attach(RIGHT,BOT)` this puts the bottom of the child onto the right anchor of the parent.
761// When an object is attached to the top or bottom its BACK direction will remaing pointing BACK.
762// When an object is attached to one of the other anchors its FRONT will be pointed DOWN and its
763// BACK pointed UP. You can change this using the `spin=` argument to attach(). Note that this spin
764// rotates around the attachment vector and is not the same as the spin argument to the child, which
765// will usually rotate around some other direction that may be hard to predict. For 2D objects you cannot
766// give spin because it is not possible to spin around the attachment vector; spinning the object around the Z axis
767// would change the child orientation so that the anchors are no longer parallel. Furthermore, any spin
768// parameter you give to the child will be ignored so that the attachment condition of parallel anchors is preserved.
769// .
770// As with {{align()}} you can use the `align=` parameter to align the child to an edge or corner of the
771// face where that child is attached. For example `attach(TOP,BOT,align=RIGHT)` would stand the child
772// up on the top while aligning it with the right edge of the top face, and `attach(RIGHT,BOT,align=TOP)` which
773// stand the object on the right face while aligning with the top edge. If you apply spin using the
774// argument to `attach()` then it will be taken into account for the alignment. If you apply spin with
775// a parameter to the child it will NOT be taken into account. Note that spin is not permitted for
776// 2D objects because it would change the child orientation so that the anchors are no longer parallel.
777// When you use `align=` you can also adjust the position using `inset=`, which shifts the child
778// away from the edge or corner it is aligned to.
779// .
780// If you give `inside=true` then the anchor arrows are lined up so they are pointing the same direction and
781// the child object will be located inside the parent. In this case a default "remove" tag is applied to
782// the children.
783// .
784// Because the attachment process forces an orientation and anchor point for the child, it overrides
785// any such specifications you give to the child: **both `anchor=` and `orient=` given to the child are
786// ignored** with the **double argument** version of `attach()`. As noted above, you can give `spin=` to the
787// child but using the `spin=` parameter to `attach()` is more likely to be useful.
788// .
789// For the single parameter version of `attach()` you give only the `parent` anchor. The `align` direction
790// is not permitted. In this case the child is placed at the specified parent anchor point
791// and rotated to the anchor direction. For example, `attach(TOP) cuboid(2);` will place a small
792// cube **with its center** located at the TOP anchor of the parent, so just half the cube will project
793// from the parent. If you want the cube sitting on the parent you need to anchor the cube to its bottom:
794// `attach(TOP) cuboid(2,anchor=BOT);`.
795// .
796// The **single argument** version of `attach()` **respects `anchor=` and `orient=` given to the child.**
797// These options will probably be necessary, in fact, to get the child correctly positioned. Note that
798// giving `spin=` to `attach()` in this case is the same as applying `zrot()` to the child.
799// .
800// Attached children may be ovarlapped into the parent a bit, as given by the `$overlap` value
801// which is 0 by default, or by the `overlap=` argument. This is to prevent OpenSCAD
802// from making non-manifold objects. You can define `$overlap=` as an argument in a parent
803// module to set the default for all attachments to it.
804// .
805// For a step-by-step explanation of
806// attachments, see the [Attachments Tutorial](Tutorial-Attachments).
807// Arguments:
808// parent = The parent anchor point to attach to or a list of parent anchor points.
809// child = Optional child anchor point. If given, orients the child to connect this anchor point to the parent anchor.
810// ---
811// align = If `child` is given you can specify alignment or list of alistnments to shift the child to an edge or corner of the parent.
812// inset = Shift aligned children away from their alignment edge/corner by this amount. Default: 0
813// overlap = Amount to sink child into the parent. Equivalent to `down(X)` after the attach. This defaults to the value in `$overlap`, which is `0` by default.
814// inside = If `child` is given you can set `inside=true` to attach the child to the inside of the parent for diff() operations. Default: false
815// shiftout = Shift an inside object outward so that it overlaps all the aligned faces. Default: 0
816// spin = Amount to rotate the parent around the axis of the parent anchor. (Only permitted in 3D.)
817// Side Effects:
818// `$anchor` set to the parent anchor value used for the child.
819// `$align` set to the align value used for the child.
820// `$idx` set to a unique index for each child, increasing by alignment first.
821// `$attach_anchor` for each anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
822// if inside is true then set default tag to "remove"
823// `$attach_to` is set to the value of the `child` argument, if given. Otherwise, `undef`
824// Example: Cylinder placed on top of cube:
825// cuboid(50)
826// attach(TOP,BOT) cylinder(d1=30,d2=15,h=25);
827// Example: Cylinder on right and front side of cube:
828// cuboid(50)
829// attach([RIGHT,FRONT],BOT) cylinder(d1=30,d2=15,h=25);
830// Example: Using `align` can align child object(s) with edges
831// prismoid(50,25,25) color("green"){
832// attach(TOP,BOT,align=[BACK,FWD]) cuboid(4);
833// attach(RIGHT,BOT,align=[TOP,BOT]) cuboid(4);
834// }
835// Example: One aligned to the corner upside down (light blue) and one inset fromt the corner (pink), one aligned on a side (orange) and one rotated and aligned (green).
836// cuboid(30) {
837// attach(TOP,TOP,align=FRONT+RIGHT) color("lightblue") prismoid(5,3,3);
838// attach(TOP,BOT,inset=3,align=FRONT+LEFT) color("pink") prismoid(5,3,3);
839// attach(FRONT,RIGHT,align=TOP) color("orange") prismoid(5,3,3);
840// attach(FRONT,RIGHT,align=RIGHT,spin=90) color("lightgreen") prismoid(5,3,3);
841// }
842// Example: Rotation not a multiple of 90 degrees with alignment. The children are aligned on a corner.
843// cuboid(30)
844// attach(FRONT,BOT,spin=33,align=[RIGHT,LEFT,TOP,BOT,RIGHT+TOP])
845// color("lightblue")cuboid(4);
846// Example: Anchoring the cone onto the sphere gives a single point of contact.
847// spheroid(d=20)
848// attach([1,1.5,1], BOTTOM) cyl(l=11.5, d1=10, d2=5);
849// Example: Using the `overlap` option can help:
850// spheroid(d=20)
851// attach([1,1.5,1], BOTTOM, overlap=1.5) cyl(l=11.5, d1=10, d2=5);
852// Example: Alignment works for cylinders but you can only align with either the top or bototm face:
853// cyl(h=30,d=10)
854// attach([LEFT,[1,1.3]], BOT,align=TOP) cuboid(6);
855// Example: Attaching to edges. The light blue and orange objects are attached to edges. The purple object is attached to an edge and aligned.
856// prismoid([20,10],[10,10],7){
857// attach(RIGHT+TOP,BOT,align=FRONT) color("pink")cuboid(2);
858// attach(BACK+TOP, BOT) color("lightblue")cuboid(2);
859// attach(RIGHT+BOT, RIGHT,spin=90) color("orange")cyl(h=8,d=1);
860// }
861// Example: Attaching inside the parent. For inside attachment the anchors are lined up pointing the same direction, so the most natural way to anchor the child is using its TOP anchor. This is equivalent to anchoring outside with the BOTTOM anchor and then lowering the child into the parent by its full depth.
862// back_half()
863// diff()
864// cuboid(20)
865// attach(TOP,TOP,inside=true,shiftout=0.01) cyl(d1=10,d2=5,h=10);
866// Example: Attaching inside the parent with alignment
867// diff()
868// cuboid(20){
869// attach(TOP,TOP,inside=true,align=RIGHT,shiftout=.01) cuboid([8,7,3]);
870// attach(TOP,TOP,inside=true,align=LEFT+FRONT,shiftout=0.01) cuboid([3,4,5]);
871// attach(RIGHT+FRONT, TOP, inside=true) cuboid([10,3,5]);
872// attach(RIGHT+FRONT, TOP, inside=true, align=TOP,shiftout=.01) cuboid([5,1,2]);
873// }
874
875module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, inside=false, from, to)
876{
877 dummy3=
878 assert(num_defined([to,child])<2, "Cannot combine deprecated 'to' argument with 'child' parameter")
879 assert(num_defined([from,parent])<2, "Cannot combine deprecated 'from' argument with 'parent' parameter");
880 if (is_def(to))
881 echo("The 'to' option to attach() is deprecated and will be removed in the future. Use 'child' instead.");
882 if (is_def(from))
883 echo("The 'from' option to attach(0 is deprecated and will be removed in the future. Use 'parent' instead");
884 if (norot)
885 echo("The 'norot' option to attach() is deprecated and will be removed in the future. Use position() instead.");
886 req_children($children);
887
888 dummy=assert($parent_geom != undef, "No object to attach to!")
889 assert(is_undef(child) || is_string(child) || (is_vector(child) && (len(child)==2 || len(child)==3)), "child must be a named anchor (a string) or a 2-vector or 3-vector")
890 assert(is_undef(align) || !is_string(child), "child is a named anchor. Named anchors are not supported with align=");
891
892 two_d = _attach_geom_2d($parent_geom);
893 overlap = (overlap!=undef)? overlap : $overlap;
894 parent = first_defined([parent,from]);
895 anchors = is_vector(parent) || is_string(parent) ? [parent] : parent;
896 align_list = is_undef(align) ? [undef]
897 : is_vector(align) || is_string(align) ? [align] : align;
898 dummy4 = assert(is_string(parent) || is_list(parent), "Invalid parent anchor or anchor list")
899 assert(spin==0 || (!two_d || is_undef(child)), "spin is not allowed for 2d objects when 'child' is given");
900 child_temp = first_defined([child,to]);
901 child = two_d ? _force_anchor_2d(child_temp) : child_temp;
902 dummy2=assert(align_list==[undef] || is_def(child), "Cannot use 'align' without 'child'")
903 assert(!inside || is_def(child), "Cannot use 'inside' without 'child'")
904 assert(inset==0 || is_def(child), "Cannot specify 'inset' without 'child'")
905 assert(shiftout==0 || is_def(child), "Cannot specify 'shiftout' without 'child'");
906 factor = inside?-1:1;
907 $attach_to = child;
908 for (anch_ind = idx(anchors)) {
909 dummy=assert(is_string(anchors[anch_ind]) || (is_vector(anchors[anch_ind]) && (len(anchors[anch_ind])==2 || len(anchors[anch_ind])==3)),
910 str("parent[",anch_ind,"] is ",anchors[anch_ind]," but it must be a named anchor (string) or a 2-vector or 3-vector"))
911 assert(align_list==[undef] || !is_string(anchors[anch_ind]),
912 str("parent[",anch_ind,"] is a named anchor (",anchors[anch_ind],"), but named anchors are not supported with align="));
913 anchor = is_string(anchors[anch_ind])? anchors[anch_ind]
914 : two_d?_force_anchor_2d(anchors[anch_ind])
915 : point3d(anchors[anch_ind]);
916 anchor_data = _find_anchor(anchor, $parent_geom);
917 anchor_pos = anchor_data[1];
918 anchor_dir = factor*anchor_data[2];
919 anchor_spin = !inside || anchor_data[3]==0 ? anchor_data[3] : 180+anchor_data[3];
920 $anchor=anchor;
921 for(align_ind = idx(align_list)){
922 align = is_undef(align_list[align_ind]) ? undef
923 : assert(is_vector(align_list[align_ind],2) || is_vector(align_list[align_ind],3), "align direction must be a 2-vector or 3-vector")
924 two_d ? _force_anchor_2d(align_list[align_ind])
925 : point3d(align_list[align_ind]);
926 $idx = align_ind+len(align_list)*anch_ind;
927 $align=align;
928 dummy=assert(is_undef(align) || all_zero(v_mul(anchor,align)),
929 str("Invalid alignment: align value (",align,") includes component parallel to parent anchor (",anchor,")"));
930 pos = is_undef(align) ? anchor_data[1] : _find_anchor(anchor+align, $parent_geom)[1];
931 $attach_anchor = list_set(anchor_data, 1, pos); ///
932 startdir = two_d || is_undef(align)? undef
933 : anchor==UP || anchor==DOWN ? BACK
934 : UP - (anchor*UP)*anchor/(anchor*anchor);
935 enddir = is_undef(child) || child.z==0 ? UP : BACK;
936 child_adjustment = is_undef(align)? CTR
937 : two_d ? rot(to=child,from=-factor*anchor,p=align)
938 : apply( frame_map(x=child, z=enddir)
939 *frame_map(x=-factor*anchor, z=startdir, reverse=true)
940 *rot(v=anchor,-spin), align);
941 $anchor_override = all_zero(child_adjustment)? inside?child:undef
942 : child+child_adjustment;
943 reference = two_d? BACK : UP;
944 inset_dir = is_undef(align) ? CTR
945 : two_d ? rot(to=reference, from=anchor,p=align)
946 : apply(zrot(-factor*spin)*frame_map(x=reference, z=BACK)*frame_map(x=factor*anchor, z=startdir, reverse=true),
947 align);
948 spinaxis = two_d? UP : anchor_dir;
949 olap = - overlap * reference - inset*inset_dir + shiftout * (inset_dir + factor*reference);
950 if (norot || (approx(anchor_dir,reference) && anchor_spin==0)) {
951 translate(pos) rot(v=spinaxis,a=factor*spin) translate(olap) default_tag("remove",inside) children();
952 } else {
953 translate(pos)
954 rot(v=spinaxis,a=factor*spin)
955 rot(anchor_spin,from=reference,to=anchor_dir){
956 translate(olap)
957 default_tag("remove",inside) children();}}
958 }
959 }
960}
961
962
963// Section: Tagging
964
965// Module: tag()
966// Synopsis: Assigns a tag to an object
967// Topics: Attachments
968// See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
969// Usage:
970// PARENT() tag(tag) CHILDREN;
971// Description:
972// Assigns the specified tag to all of the children. Note that if you want
973// to apply a tag to non-tag-aware objects you need to use {{force_tag()}} instead.
974// This works by setting the `$tag` variable, but it provides extra error checking and
975// handling of scopes. You may set `$tag` directly yourself, but this is not recommended.
976// .
977// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
978// Arguments:
979// tag = tag string, which must not contain any spaces.
980// Side Effects:
981// Sets `$tag` to the tag you specify, possibly with a scope prefix.
982// Example(3D): Applies the tag to both cuboids instead of having to repeat `$tag="remove"` for each one.
983// diff("remove")
984// cuboid(10){
985// position(TOP) cuboid(3);
986// tag("remove")
987// {
988// position(FRONT) cuboid(3);
989// position(RIGHT) cuboid(3);
990// }
991// }
992module tag(tag)
993{
994 req_children($children);
995 check=
996 assert(is_string(tag),"tag must be a string")
997 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
998 $tag = str($tag_prefix,tag);
999 children();
1000}
1001
1002
1003// Module: force_tag()
1004// Synopsis: Assigns a tag to a non-attachable object.
1005// Topics: Attachments
1006// See Also: tag(), recolor(), hide(), show_only(), diff(), intersect()
1007// Usage:
1008// PARENT() force_tag([tag]) CHILDREN;
1009// Description:
1010// You use this module when you want to make a non-attachable or non-BOSL2 module respect tags.
1011// It applies to its children the tag specified (or the tag currently in force if you don't specify a tag),
1012// making a final determination about whether to show or hide the children.
1013// This means that tagging in children's children will be ignored.
1014// This module is specifically provided for operating on children that are not tag aware such as modules
1015// that don't use {{attachable()}} or built in modules such as
1016// - `polygon()`
1017// - `projection()`
1018// - `polyhedron()` (or use [`vnf_polyhedron()`](vnf.scad#vnf_polyhedron))
1019// - `linear_extrude()` (or use [`linear_sweep()`](regions.scad#linear_sweep))
1020// - `rotate_extrude()`
1021// - `surface()`
1022// - `import()`
1023// - `difference()`
1024// - `intersection()`
1025// - `hull()`
1026// .
1027// When you use tag-based modules like {{diff()}} with a non-attachable module, the result may be puzzling.
1028// Any time a test occurs for display of child() that test will succeed. This means that when diff() checks
1029// to see if it should show a module it will show it, and when diff() checks to see if it should subtract the module
1030// it will subtract it. The result will be a hole, possibly with zero-thickness edges or faces. In order to
1031// get the correct behavior, every non-attachable module needs an invocation of force_tag, even ones
1032// that are not tagged.
1033// .
1034// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1035// Arguments:
1036// tag = tag string, which must not contain any spaces
1037// Side Effects:
1038// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1039// Example(2D): This example produces the full square without subtracting the "remove" item. When you use non-attachable modules with tags, results are unpredictable.
1040// diff()
1041// {
1042// polygon(square(10));
1043// move(-[.01,.01])polygon(square(5),$tag="remove");
1044// }
1045// Example(2D): Adding force_tag() fixes the model. Note you need to add it to *every* non-attachable module, even the untagged ones, as shown here.
1046// diff()
1047// {
1048// force_tag()
1049// polygon(square(10));
1050// force_tag("remove")
1051// move(-[.01,.01])polygon(square(5));
1052// }
1053module force_tag(tag)
1054{
1055 req_children($children);
1056 check1=assert(is_undef(tag) || is_string(tag),"tag must be a string");
1057 $tag = str($tag_prefix,default(tag,$tag));
1058 assert(undef==str_find($tag," "),str("Tag string \"",$tag,"\" contains a space, which is not allowed"));
1059 if(_is_shown())
1060 show_all()
1061 children();
1062}
1063
1064
1065
1066// Module: default_tag()
1067// Synopsis: Sets a default tag for all children.
1068// Topics: Attachments
1069// See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect()
1070// Usage:
1071// PARENT() default_tag(tag) CHILDREN;
1072// Description:
1073// Sets a default tag for all of the children. This is intended to be used to set a tag for a whole module
1074// that is then used outside the module, such as setting the tag to "remove" for easy operation with {{diff()}}.
1075// The default_tag() module sets the `$tag` variable only if it is not already
1076// set so you can have a module set a default tag of "remove" but that tag can be overridden by a {{tag()}}
1077// in force from a parent. If you use {{tag()}} it will override any previously
1078// specified tag from a parent, which can be very confusing to a user trying to change the tag on a module.
1079// The `do_tag` parameter allows you to apply a default tag conditionally without having to repeat the children.
1080// .
1081// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1082// Arguments:
1083// tag = tag string, which must not contain any spaces.
1084// do_tag = if false do not set the tag.
1085// Side Effects:
1086// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1087// Example(3D): The module thing() is defined with {{tag()}} and the user applied tag of "keep_it" is ignored, leaving the user puzzled.
1088// module thing() { tag("remove") cuboid(10);}
1089// diff()
1090// cuboid(20){
1091// position(TOP) thing();
1092// position(RIGHT) tag("keep_it") thing();
1093// }
1094// Example(3D): Using default_tag() fixes this problem: the user applied tag does not get overridden by the tag hidden in the module definition.
1095// module thing() { default_tag("remove") cuboid(10);}
1096// diff()
1097// cuboid(20){
1098// position(TOP) thing();
1099// position(RIGHT) tag("keep_it") thing();
1100// }
1101module default_tag(tag,do_tag=true)
1102{
1103 if ($tag=="" && do_tag) tag(tag) children();
1104 else children();
1105}
1106
1107
1108// Module: tag_scope()
1109// Synopsis: Creates a new tag scope.
1110// See Also: tag(), force_tag(), default_tag()
1111// Topics: Attachments
1112// Usage:
1113// tag_scope([scope]) CHILDREN;
1114// Description:
1115// Creates a tag scope with locally altered tag names to avoid tag name conflict with other code.
1116// This is necessary when writing modules because the module's caller might happen to use the same tags.
1117// Note that if you directly set the `$tag` variable then tag scoping will not work correctly.
1118// Side Effects:
1119// `$tag_prefix` is set to the value of `scope=` if given, otherwise is set to a random string.
1120// Example: In this example the ring module uses "remove" tags which will conflict with use of the same tags by the parent.
1121// module ring(r,h,w=1,anchor,spin,orient)
1122// {
1123// tag_scope("ringscope")
1124// attachable(anchor,spin,orient,r=r,h=h){
1125// diff()
1126// cyl(r=r,h=h)
1127// tag("remove") cyl(r=r-w,h=h+1);
1128// children();
1129// }
1130// }
1131// // Calling the module using "remove" tags
1132// // will conflict with internal tag use in
1133// // the ring module.
1134// $fn=32;
1135// diff(){
1136// ring(10,7,w=4);
1137// tag("remove")ring(8,8);
1138// tag("remove")diff("rem"){
1139// ring(9.5,8,w=1);
1140// tag("rem")ring(9.5,8,w=.3);
1141// }
1142// }
1143module tag_scope(scope){
1144 req_children($children);
1145 scope = is_undef(scope) ? rand_str(20) : scope;
1146 assert(is_string(scope), "scope must be a string");
1147 assert(undef==str_find(scope," "),str("Scope string \"",scope,"\" contains a space, which is not allowed"));
1148 $tag_prefix=scope;
1149 children();
1150}
1151
1152
1153// Section: Attachment Modifiers
1154
1155// Module: diff()
1156// Synopsis: Performs a differencing operation using tags rather than hierarchy to control what happens.
1157// Topics: Attachments
1158// See Also: tag(), force_tag(), recolor(), show_only(), hide(), tag_diff(), intersect(), tag_intersect()
1159// Usage:
1160// diff([remove], [keep]) PARENT() CHILDREN;
1161// Description:
1162// Performs a differencing operation using tags to control what happens. This is specifically intended to
1163// address the situation where you want differences between a parent and child object, something
1164// that is impossible with the native difference() module.
1165// The children to diff are grouped into three categories, regardless of nesting level.
1166// The `remove` argument is a space delimited list of tags specifying objects to
1167// subtract. The `keep` argument is a similar list of tags giving objects to be kept.
1168// Objects not matching either the `remove` or `keep` lists form the third category of base objects.
1169// To produce its output, diff() forms the union of all the base objects and then
1170// subtracts all the objects with tags in `remove`. Finally it adds in objects listed in `keep`.
1171// Attachable objects should be tagged using {{tag()}}
1172// and non-attachable objects with {{force_tag()}}.
1173// .
1174// Remember when using tagged operations with that the operations don't happen in hierarchical order, since
1175// the point of tags is to break the hierarchy. If you tag an object with a keep tag, nothing will be
1176// subtracted from it, no matter where it appears because kept objects are unioned in at the end.
1177// If you want a child of an object tagged with a remove tag to stay in the model it may be
1178// better to give it a tag that is not a remove tag or a keep tag. Such an object *will* be subject to
1179// subtractions from other remove-tagged objects.
1180// .
1181// Note that `diff()` invokes its children three times.
1182// .
1183// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1184// Arguments:
1185// remove = String containing space delimited set of tag names of children to difference away. Default: `"remove"`
1186// keep = String containing space delimited set of tag names of children to keep; that is, to union into the model after differencing is completed. Default: `"keep"`
1187// Example: Diffing using default tags
1188// diff()
1189// cuboid(50) {
1190// tag("remove") attach(TOP) sphere(d=40);
1191// tag("keep") attach(CTR) cylinder(h=40, d=10);
1192// }
1193// Example: The "hole" items are subtracted from everything else. The other tags can be anything you find convenient.
1194// diff("hole")
1195// tag("body")sphere(d=100) {
1196// tag("pole") zcyl(d=55, h=100); // attach() not needed for center-to-center.
1197// tag("hole") {
1198// xcyl(d=55, h=101);
1199// ycyl(d=55, h=101);
1200// }
1201// tag("axle")zcyl(d=15, h=140);
1202// }
1203// Example:
1204// diff(keep="axle")
1205// sphere(d=100) {
1206// tag("axle")xcyl(d=40, l=120);
1207// tag("remove")cuboid([40,120,100]);
1208// }
1209// Example: Masking
1210// diff()
1211// cube([80,90,100], center=true) {
1212// edge_mask(FWD)
1213// rounding_edge_mask(l=max($parent_size)*1.01, r=25);
1214// }
1215// Example: Here we subtract the parent object from the child. Because tags propagate to children we need to clear the "remove" tag from the child.
1216// diff()
1217// tag("remove")cuboid(10)
1218// tag("")position(RIGHT+BACK)cyl(r=8,h=9);
1219// Example(3D,VPR=[104,0,200], VPT=[-0.9,3.03, -0.74], VPD=19,NoAxes,NoScales): A pipe module that subtracts its interior when you call it using diff(). Normally if you union two pipes together, you'll get interfering walls at the intersection, but not here:
1220// $fn=16;
1221// // This module must be called by subtracting with "diff"
1222// module pipe(length, od, id) {
1223// // Strip the tag the user is using to subtract
1224// tag("")cylinder(h=length, d=od, center=true);
1225// // Leave the tag alone here, so this one is removed
1226// cylinder(h=length+.02, d=id, center=true);
1227// }
1228// // Draw some intersecting pipes
1229// diff(){
1230// tag("remove"){
1231// pipe(length=5, od=2, id=1.9);
1232// zrot(10)xrot(75)
1233// pipe(length=5, od=2, id=1.9);
1234// }
1235// // The orange bar has its center removed
1236// color("orange") down(1) xcyl(h=8, d=1);
1237// // "keep" preserves the interior of the blue bar intact
1238// tag("keep") recolor("blue") up(1) xcyl(h=8, d=1);
1239// }
1240// // Objects outside the diff don't have pipe interiors removed
1241// color("purple") down(2.2) ycyl(h=8, d=0.3);
1242// Example(3D,NoScales,NoAxes): Nested diff() calls work as expected, but be careful of reusing tag names, even hidden in submodules.
1243// $fn=32;
1244// diff("rem1")
1245// cyl(r=10,h=10){
1246// diff("rem2",$tag="rem1"){
1247// cyl(r=8,h=11);
1248// tag("rem2")diff("rem3"){
1249// cyl(r=6,h=12);
1250// tag("rem3")cyl(r=4,h=13);
1251// }
1252// }
1253// }
1254// Example: This example shows deep nesting, where all the differences cross levels. Unlike the preceding example, each cylinder is positioned relative to its parent. Note that it suffices to use two remove tags, alternating between them at each level.
1255// $fn=32;
1256// diff("remA")
1257// cyl(r=9, h=6)
1258// tag("remA")diff("remB")
1259// left(.2)position(RIGHT)cyl(r=8,h=7,anchor=RIGHT)
1260// tag("remB")diff("remA")
1261// left(.2)position(LEFT)cyl(r=7,h=7,anchor=LEFT)
1262// tag("remA")diff("remB")
1263// left(.2)position(LEFT)cyl(r=6,h=8,anchor=LEFT)
1264// tag("remB")diff("remA")
1265// right(.2)position(RIGHT)cyl(r=5,h=9,anchor=RIGHT)
1266// tag("remA")diff("remB")
1267// right(.2)position(RIGHT)cyl(r=4,h=10,anchor=RIGHT)
1268// tag("remB")left(.2)position(LEFT)cyl(r=3,h=11,anchor=LEFT);
1269// Example(3D,NoAxes,NoScales): When working with Non-Attachables like rotate_extrude() you must apply {{force_tag()}} to every non-attachable object.
1270// back_half()
1271// diff("remove")
1272// cuboid(40) {
1273// attach(TOP)
1274// recolor("lightgreen")
1275// cyl(l=10,d=30);
1276// position(TOP+RIGHT)
1277// force_tag("remove")
1278// xrot(90)
1279// rotate_extrude()
1280// right(20)
1281// circle(5);
1282// }
1283// Example: Here is another example where two children are intersected using the native intersection operator, and then tagged with {{force_tag()}}. Note that because the children are at the same level, you don't need to use a tagged operator for their intersection.
1284// $fn=32;
1285// diff()
1286// cuboid(10){
1287// force_tag("remove")intersection()
1288// {
1289// position(RIGHT) cyl(r=7,h=15);
1290// position(LEFT) cyl(r=7,h=15);
1291// }
1292// tag("keep")cyl(r=1,h=9);
1293// }
1294// Example: In this example the children that are subtracted are each at different nesting levels, with a kept object in between.
1295// $fn=32;
1296// diff()
1297// cuboid(10){
1298// tag("remove")cyl(r=4,h=11)
1299// tag("keep")cyl(r=3,h=17)
1300// tag("remove")position(RIGHT)cyl(r=2,h=18);
1301// }
1302// Example: Combining tag operators can be tricky. Here the `diff()` operation keeps two tags, "fullkeep" and "keep". Then {{intersect()}} intersects the "keep" tagged item with everything else, but keeps the "fullkeep" object.
1303// $fn=32;
1304// intersect("keep","fullkeep")
1305// diff(keep="fullkeep keep")
1306// cuboid(10){
1307// tag("remove")cyl(r=4,h=11);
1308// tag("keep") position(RIGHT)cyl(r=8,h=12);
1309// tag("fullkeep")cyl(r=1,h=12);
1310// }
1311// Example: In this complex example we form an intersection, subtract an object, and keep some objects. Note that for the small cylinders on either side, marking them as "keep" or removing their tag gives the same effect. This is because without a tag they become part of the intersection and the result ends up the same. For the two cylinders at the back, however, the result is different. With "keep" the cylinder on the left appears whole, but without it, the cylinder at the back right is subject to intersection.
1312// $fn=64;
1313// diff()
1314// intersect(keep="remove keep")
1315// cuboid(10,$thing="cube"){
1316// tag("intersect"){
1317// position(RIGHT) cyl(r=5.5,h=15)
1318// tag("")cyl(r=2,h=10);
1319// position(LEFT) cyl(r=5.54,h=15)
1320// tag("keep")cyl(r=2,h=10);
1321// }
1322// // Untagged it is in the intersection
1323// tag("") position(BACK+RIGHT)
1324// cyl(r=2,h=10,anchor=CTR);
1325// // With keep the full cylinder appears
1326// tag("keep") position(BACK+LEFT)
1327// cyl(r=2,h=10,anchor=CTR);
1328// tag("remove") cyl(r=3,h=15);
1329// }
1330module diff(remove="remove", keep="keep")
1331{
1332 req_children($children);
1333 assert(is_string(remove),"remove must be a string of tags");
1334 assert(is_string(keep),"keep must be a string of tags");
1335 if (_is_shown())
1336 {
1337 difference() {
1338 hide(str(remove," ",keep)) children();
1339 show_only(remove) children();
1340 }
1341 }
1342 show_int(keep)children();
1343}
1344
1345
1346// Module: tag_diff()
1347// Synopsis: Performs a {{diff()}} and then sets a tag on the result.
1348// Topics: Attachments
1349// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), intersect(), tag_intersect()
1350// Usage:
1351// tag_diff(tag, [remove], [keep]) PARENT() CHILDREN;
1352// Description:
1353// Perform a differencing operation in the manner of {{diff()}} using tags to control what happens,
1354// and then tag the resulting difference object with the specified tag. This forces the specified
1355// tag to be resolved at the level of the difference operation. In most cases, this is not necessary,
1356// but if you have kept objects and want to operate on this difference object as a whole object using
1357// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1358// .
1359// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1360// Arguments:
1361// tag = Tag string to apply to this difference object
1362// remove = String containing space delimited set of tag names of children to difference away. Default: `"remove"`
1363// keep = String containing space delimited set of tag names of children to keep; that is, to union into the model after differencing is completed. Default: `"keep"`
1364// Side Effects:
1365// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1366// Example: In this example we have a difference with a kept object that is then subtracted from a cube, but we don't want the kept object to appear in the final output, so this result is wrong:
1367// diff("rem"){
1368// cuboid([20,10,30],anchor=FRONT);
1369// tag("rem")diff("remove","keep"){
1370// cuboid([10,10,20]);
1371// tag("remove")cuboid([11,11,5]);
1372// tag("keep")cuboid([2,2,20]);
1373// }
1374// }
1375// Example: Using tag_diff corrects the problem:
1376// diff("rem"){
1377// cuboid([20,10,30],anchor=FRONT);
1378// tag_diff("rem","remove","keep"){
1379// cuboid([10,10,20]);
1380// tag("remove")cuboid([11,11,5]);
1381// tag("keep")cuboid([2,2,20]);
1382// }
1383// }
1384// Example: This concentric cylinder example uses "keep" and produces the wrong result. The kept cylinder gets kept in the final output instead of subtracted. This happens even when we make sure to change the `keep` argument at the top level {{diff()}} call.
1385// diff("rem","nothing")
1386// cyl(r=8,h=6)
1387// tag("rem")diff()
1388// cyl(r=7,h=7)
1389// tag("remove")cyl(r=6,h=8)
1390// tag("keep")cyl(r=5,h=9);
1391// Example: Changing to tag_diff() causes the kept cylinder to be subtracted, producing the desired result:
1392// diff("rem")
1393// cyl(r=8,h=6)
1394// tag_diff("rem")
1395// cyl(r=7,h=7)
1396// tag("remove")cyl(r=6,h=8)
1397// tag("keep")cyl(r=5,h=9);
1398module tag_diff(tag,remove="remove", keep="keep")
1399{
1400 req_children($children);
1401 assert(is_string(remove),"remove must be a string of tags");
1402 assert(is_string(keep),"keep must be a string of tags");
1403 assert(is_string(tag),"tag must be a string");
1404 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1405 $tag=str($tag_prefix,tag);
1406 if (_is_shown())
1407 show_all(){
1408 difference() {
1409 hide(str(remove," ",keep)) children();
1410 show_only(remove) children();
1411 }
1412 show_only(keep)children();
1413 }
1414}
1415
1416
1417// Module: intersect()
1418// Synopsis: Perform an intersection operation on children using tags rather than hierarchy to control what happens.
1419// Topics: Attachments
1420// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), tag_intersect()
1421// Usage:
1422// intersect([intersect], [keep]) PARENT() CHILDREN;
1423// Description:
1424// Performs an intersection operation on its children, using tags to
1425// determine what happens. This is specifically intended to address
1426// the situation where you want intersections involving a parent and
1427// child object, something that is impossible with the native
1428// intersection() module. This module treats the children in three
1429// groups: objects matching the tags listed in `intersect`, objects
1430// matching tags listed in `keep`, and the remaining objects that
1431// don't match any of the listed tags. The intersection is computed
1432// between the union of the `intersect` tagged objects and union of the objects that don't
1433// match any of the listed tags. Finally the objects listed in `keep` are
1434// unioned with the result. Attachable objects should be tagged using {{tag()}}
1435// and non-attachable objects with {{force_tag()}}.
1436// .
1437// Note that `intersect()` invokes its children three times.
1438// .
1439// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1440// Arguments:
1441// intersect = String containing space delimited set of tag names of children to intersect. Default: "intersect"
1442// keep = String containing space delimited set of tag names of children to keep whole. Default: "keep"
1443// Example:
1444// intersect("mask", keep="axle")
1445// sphere(d=100) {
1446// tag("mask")cuboid([40,100,100]);
1447// tag("axle")xcyl(d=40, l=100);
1448// }
1449// Example: Combining tag operators can be tricky. Here the {{diff()}} operation keeps two tags, "fullkeep" and "keep". Then `intersect()` intersects the "keep" tagged item with everything else, but keeps the "fullkeep" object.
1450// $fn=32;
1451// intersect("keep","fullkeep")
1452// diff(keep="fullkeep keep")
1453// cuboid(10){
1454// tag("remove")cyl(r=4,h=11);
1455// tag("keep") position(RIGHT)cyl(r=8,h=12);
1456// tag("fullkeep")cyl(r=1,h=12);
1457// }
1458// Example: In this complex example we form an intersection, subtract an object, and keep some objects. Note that for the small cylinders on either side, marking them as "keep" or removing their tag gives the same effect. This is because without a tag they become part of the intersection and the result ends up the same. For the two cylinders at the back, however, the result is different. With "keep" the cylinder on the left appears whole, but without it, the cylinder at the back right is subject to intersection.
1459// $fn=64;
1460// diff()
1461// intersect(keep="remove keep")
1462// cuboid(10,$thing="cube"){
1463// tag("intersect"){
1464// position(RIGHT) cyl(r=5.5,h=15)
1465// tag("")cyl(r=2,h=10);
1466// position(LEFT) cyl(r=5.54,h=15)
1467// tag("keep")cyl(r=2,h=10);
1468// }
1469// // Untagged it is in the intersection
1470// tag("") position(BACK+RIGHT)
1471// cyl(r=2,h=10,anchor=CTR);
1472// // With keep the full cylinder appears
1473// tag("keep") position(BACK+LEFT)
1474// cyl(r=2,h=10,anchor=CTR);
1475// tag("remove") cyl(r=3,h=15);
1476// }
1477module intersect(intersect="intersect",keep="keep")
1478{
1479 assert(is_string(intersect),"intersect must be a string of tags");
1480 assert(is_string(keep),"keep must be a string of tags");
1481 intersection(){
1482 show_only(intersect) children();
1483 hide(str(intersect," ",keep)) children();
1484 }
1485 show_int(keep) children();
1486}
1487
1488
1489// Module: tag_intersect()
1490// Synopsis: Performs an {{intersect()}} and then tags the result.
1491// Topics: Attachments
1492// See Also: tag(), force_tag(), recolor(), show_only(), hide(), diff(), tag_diff(), intersect()
1493// Usage:
1494// tag_intersect(tag, [intersect], [keep]) PARENT() CHILDREN;
1495// Description:
1496// Perform an intersection operation in the manner of {{intersect()}} using tags to control what happens,
1497// and then tag the resulting difference object with the specified tag. This forces the specified
1498// tag to be resolved at the level of the intersect operation. In most cases, this is not necessary,
1499// but if you have kept objects and want to operate on this difference object as a whole object using
1500// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1501// .
1502// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1503// Arguments:
1504// tag = Tag to set for the intersection
1505// intersect = String containing space delimited set of tag names of children to intersect. Default: "intersect"
1506// keep = String containing space delimited set of tag names of children to keep whole. Default: "keep"
1507// Side Effects:
1508// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1509// Example: Without `tag_intersect()` the kept object is not included in the difference.
1510// $fn=32;
1511// diff()
1512// cuboid([20,15,9])
1513// tag("remove")intersect()
1514// cuboid(10){
1515// tag("intersect")position(RIGHT) cyl(r=7,h=10);
1516// tag("keep")position(LEFT)cyl(r=4,h=10);
1517// }
1518// Example: Using tag_intersect corrects the problem.
1519// $fn=32;
1520// diff()
1521// cuboid([20,15,9])
1522// tag_intersect("remove")
1523// cuboid(10){
1524// tag("intersect")position(RIGHT) cyl(r=7,h=10);
1525// tag("keep")position(LEFT)cyl(r=4,h=10);
1526// }
1527module tag_intersect(tag,intersect="intersect",keep="keep")
1528{
1529 assert(is_string(intersect),"intersect must be a string of tags");
1530 assert(is_string(keep),"keep must be a string of tags");
1531 assert(is_string(tag),"tag must be a string");
1532 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1533 $tag=str($tag_prefix,tag);
1534 if (_is_shown())
1535 show_all(){
1536 intersection(){
1537 show_only(intersect) children();
1538 hide(str(intersect," ",keep)) children();
1539 }
1540 show_only(keep) children();
1541 }
1542}
1543
1544
1545// Module: conv_hull()
1546// Synopsis: Performs a hull operation on the children using tags to determine what happens.
1547// Topics: Attachments, Hulling
1548// See Also: tag(), recolor(), show_only(), hide(), diff(), intersect(), hull()
1549// Usage:
1550// conv_hull([keep]) CHILDREN;
1551// Description:
1552// Performs a hull operation on the children using tags to determine what happens. The items
1553// not tagged with the `keep` tags are combined into a convex hull, and the children tagged with the keep tags
1554// are unioned with the result.
1555// .
1556// Note that `conv_hull()` invokes its children twice.
1557// .
1558// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1559// Arguments:
1560// keep = String containing space delimited set of tag names of children to keep out of the hull. Default: "keep"
1561// Example:
1562// conv_hull("keep")
1563// sphere(d=100, $fn=64) {
1564// cuboid([40,90,90]);
1565// tag("keep")xcyl(d=40, l=120);
1566// }
1567// Example: difference combined with hull where all objects are relative to each other.
1568// $fn=32;
1569// diff()
1570// conv_hull("remove")
1571// cuboid(10)
1572// position(RIGHT+BACK)cyl(r=4,h=10)
1573// tag("remove")cyl(r=2,h=12);
1574module conv_hull(keep="keep")
1575{
1576 req_children($children);
1577 assert(is_string(keep),"keep must be a string of tags");
1578 if (_is_shown())
1579 hull() hide(keep) children();
1580 show_int(keep) children();
1581}
1582
1583
1584// Module: tag_conv_hull()
1585// Synopsis: Performs a {{conv_hull()}} and then sets a tag on the result.
1586// Topics: Attachments
1587// See Also: tag(), recolor(), show_only(), hide(), diff(), intersect()
1588// Usage:
1589// tag_conv_hull(tag, [keep]) CHILDREN;
1590// Description:
1591// Perform a convex hull operation in the manner of {{conv_hull()}} using tags to control what happens,
1592// and then tag the resulting hull object with the specified tag. This forces the specified
1593// tag to be resolved at the level of the hull operation. In most cases, this is not necessary,
1594// but if you have kept objects and want to operate on the hull object as a whole object using
1595// more tag operations, you will probably not get the results you want if you simply use {{tag()}}.
1596// .
1597// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1598// Arguments:
1599// keep = String containing space delimited set of tag names of children to keep out of the hull. Default: "keep"
1600// Side Effects:
1601// Sets `$tag` to the tag you specify, possibly with a scope prefix.
1602// Example: With a regular tag, the kept object is not handled as desired:
1603// diff(){
1604// cuboid([30,30,9])
1605// tag("remove")conv_hull("remove")
1606// cuboid(10,anchor=LEFT+FRONT){
1607// position(RIGHT+BACK)cyl(r=4,h=10);
1608// tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
1609// }
1610// }
1611// Example: Using `tag_conv_hull()` fixes the problem:
1612// diff(){
1613// cuboid([30,30,9])
1614// tag_conv_hull("remove")
1615// cuboid(10,anchor=LEFT+FRONT){
1616// position(RIGHT+BACK)cyl(r=4,h=10);
1617// tag("keep")position(FRONT+LEFT)cyl(r=4,h=10);
1618// }
1619// }
1620module tag_conv_hull(tag,keep="keep")
1621{
1622 req_children($children);
1623 assert(is_string(keep),"keep must be a string of tags");
1624 assert(is_string(tag),"tag must be a string");
1625 assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed"));
1626 $tag=str($tag_prefix,tag);
1627 if (_is_shown())
1628 show_all(){
1629 hull() hide(keep) children();
1630 show_only(keep) children();
1631 }
1632}
1633
1634
1635// Module: hide()
1636// Synopsis: Hides attachable children with the given tags.
1637// Topics: Attachments
1638// See Also: tag(), recolor(), show_only(), show_all(), show_int(), diff(), intersect()
1639// Usage:
1640// hide(tags) CHILDREN;
1641// Description:
1642// Hides all attachable children with the given tags, which you supply as a space separated string. Previously hidden objects remain hidden, so hiding is cumulative, unlike `show_only()`.
1643// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1644// Side Effects:
1645// Sets `$tags_hidden` to include the tags you specify.
1646// Example: Hides part of the model.
1647// hide("A")
1648// tag("main") cube(50, anchor=CENTER, $tag="Main") {
1649// tag("A")attach(LEFT, BOTTOM) cylinder(d=30, h=30);
1650// tag("B")attach(RIGHT, BOTTOM) cylinder(d=30, h=30);
1651// }
1652// Example: Use an invisible parent to position children. Note that children must be retagged because they inherit the parent tag.
1653// $fn=16;
1654// hide("hidden")
1655// tag("hidden")cuboid(10)
1656// tag("visible") {
1657// position(RIGHT) cyl(r=1,h=12);
1658// position(LEFT) cyl(r=1,h=12);
1659// }
1660module hide(tags)
1661{
1662 req_children($children);
1663 dummy=assert(is_string(tags), "tags must be a string");
1664 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1665 $tags_hidden = concat($tags_hidden,taglist);
1666 children();
1667}
1668
1669
1670// Module: show_only()
1671// Synopsis: Show only the children with the listed tags.
1672// See Also: tag(), recolor(), show_all(), show_int(), diff(), intersect()
1673// Topics: Attachments
1674// Usage:
1675// show_only(tags) CHILDREN;
1676// Description:
1677// Show only the children with the listed tags, which you sply as a space separated string. Only unhidden objects will be shown, so if an object is hidden either before or after the `show_only()` call then it will remain hidden. This overrides any previous `show_only()` calls. Unlike `hide()`, calls to `show_only()` are not cumulative.
1678// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1679// Side Effects:
1680// Sets `$tags_shown` to the tag you specify.
1681// Example: Display the attachments but not the parent
1682// show_only("visible")
1683// cube(50, anchor=CENTER)
1684// tag("visible"){
1685// attach(LEFT, BOTTOM) cylinder(d=30, h=30);
1686// attach(RIGHT, BOTTOM) cylinder(d=30, h=30);
1687// }
1688module show_only(tags)
1689{
1690 req_children($children);
1691 dummy=assert(is_string(tags), str("tags must be a string",tags));
1692 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1693 $tags_shown = taglist;
1694 children();
1695}
1696
1697// Module: show_all()
1698// Synopsis: Shows all children and clears tags.
1699// See Also: tag(), recolor(), show_only(), show_int(), diff(), intersect()
1700// Topics: Attachments
1701// Usage;
1702// show_all() CHILDREN;
1703// Description:
1704// Shows all children. Clears the list of hidden tags and shown tags so that all child objects will be
1705// fully displayed.
1706// Side Effects:
1707// Sets `$tags_shown="ALL"`
1708// Sets `$tags_hidden=[]`
1709module show_all()
1710{
1711 req_children($children);
1712 $tags_shown="ALL";
1713 $tags_hidden=[];
1714 children();
1715}
1716
1717
1718// Module: show_int()
1719// Synopsis: Shows children with the listed tags which were already shown in the parent context.
1720// See Also: tag(), recolor(), show_only(), show_all(), show_int(), diff(), intersect()
1721// Topics: Attachments
1722// Usage:
1723// show_int(tags) CHILDREN;
1724// Description:
1725// Show only the children with the listed tags which were already shown in the parent context.
1726// This intersects the current show list with the list of tags you provide.
1727// Arguments:
1728// tags = list of tags to show
1729// Side Effects:
1730// Sets `$tags_shown`
1731module show_int(tags)
1732{
1733 req_children($children);
1734 dummy=assert(is_string(tags), str("tags must be a string",tags));
1735 taglist = [for(s=str_split(tags," ",keep_nulls=false)) str($tag_prefix,s)];
1736 $tags_shown = $tags_shown == "ALL" ? taglist : set_intersection($tags_shown,taglist);
1737 children();
1738}
1739
1740
1741// Section: Mask Attachment
1742
1743
1744// Module: face_mask()
1745// Synopsis: Ataches a 3d mask shape to the given faces of the parent.
1746// SynTags: Trans
1747// Topics: Attachments, Masking
1748// See Also: attachable(), position(), attach(), edge_mask(), corner_mask(), face_profile(), edge_profile(), corner_profile()
1749// Usage:
1750// PARENT() face_mask(faces) CHILDREN;
1751// Description:
1752// Takes a 3D mask shape, and attaches it to the given faces, with the appropriate orientation to be
1753// differenced away. The mask shape should be vertically oriented (Z-aligned) with the bottom half
1754// (Z-) shaped to be diffed away from the face of parent attachable shape. If no tag is set then
1755// `face_mask()` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1756// For details on specifying the faces to mask see [Specifying Faces](attachments.scad#subsection-specifying-faces).
1757// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1758// Arguments:
1759// edges = Faces to mask. See [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces. Default: All faces
1760// Side Effects:
1761// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1762// `$idx` is set to the index number of each face in the list of faces given.
1763// `$attach_anchor` is set for each face given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1764// Example:
1765// diff()
1766// cylinder(r=30, h=60)
1767// face_mask(TOP) {
1768// rounding_cylinder_mask(r=30,rounding=5);
1769// cuboid([5,61,10]);
1770// }
1771// Example: Using `$idx`
1772// diff()
1773// cylinder(r=30, h=60)
1774// face_mask([TOP, BOT])
1775// zrot(45*$idx) zrot_copies([0,90]) cuboid([5,61,10]);
1776module face_mask(faces=[LEFT,RIGHT,FRONT,BACK,BOT,TOP]) {
1777 req_children($children);
1778 faces = is_vector(faces)? [faces] : faces;
1779 assert(all([for (face=faces) is_vector(face) && sum([for (x=face) x!=0? 1 : 0])==1]), "Vector in faces doesn't point at a face.");
1780 assert($parent_geom != undef, "No object to attach to!");
1781 attach(faces) {
1782 default_tag("remove") children();
1783 }
1784}
1785
1786
1787// Module: edge_mask()
1788// Synopsis: Attaches a 3D mask shape to the given edges of the parent.
1789// SynTags: Trans
1790// Topics: Attachments, Masking
1791// See Also: attachable(), position(), attach(), face_mask(), corner_mask(), face_profile(), edge_profile(), corner_profile()
1792// Usage:
1793// PARENT() edge_mask([edges], [except]) CHILDREN;
1794// Description:
1795// Takes a 3D mask shape, and attaches it to the given edges, with the appropriate orientation to be
1796// differenced away. The mask shape should be vertically oriented (Z-aligned) with the back-right
1797// quadrant (X+Y+) shaped to be diffed away from the edge of parent attachable shape. If no tag is set
1798// then `edge_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1799// For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
1800// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1801// Figure: A Typical Edge Rounding Mask
1802// module roundit(l,r) difference() {
1803// translate([-1,-1,-l/2])
1804// cube([r+1,r+1,l]);
1805// translate([r,r])
1806// cylinder(h=l+1,r=r,center=true, $fn=quantup(segs(r),4));
1807// }
1808// roundit(l=30,r=10);
1809// Arguments:
1810// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
1811// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
1812// Side Effects:
1813// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1814// `$idx` is set to the index number of each edge.
1815// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1816// `$parent_size` is set to the size of the parent object.
1817// Example:
1818// diff()
1819// cube([50,60,70],center=true)
1820// edge_mask([TOP,"Z"],except=[BACK,TOP+LEFT])
1821// rounding_edge_mask(l=71,r=10);
1822module edge_mask(edges=EDGES_ALL, except=[]) {
1823 req_children($children);
1824 assert($parent_geom != undef, "No object to attach to!");
1825 edges = _edges(edges, except=except);
1826 vecs = [
1827 for (i = [0:3], axis=[0:2])
1828 if (edges[axis][i]>0)
1829 EDGE_OFFSETS[axis][i]
1830 ];
1831 for ($idx = idx(vecs)) {
1832 vec = vecs[$idx];
1833 vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
1834 dummy=assert(vcount == 2, "Not an edge vector!");
1835 anch = _find_anchor(vec, $parent_geom);
1836 $attach_to = undef;
1837 $attach_anchor = anch;
1838 rotang =
1839 vec.z<0? [90,0,180+v_theta(vec)] :
1840 vec.z==0 && sign(vec.x)==sign(vec.y)? 135+v_theta(vec) :
1841 vec.z==0 && sign(vec.x)!=sign(vec.y)? [0,180,45+v_theta(vec)] :
1842 [-90,0,180+v_theta(vec)];
1843 translate(anch[1]) rot(rotang)
1844 default_tag("remove") children();
1845 }
1846}
1847
1848
1849// Module: corner_mask()
1850// Synopsis: Attaches a 3d mask shape to the given corners of the parent.
1851// SynTags: Trans
1852// Topics: Attachments, Masking
1853// See Also: attachable(), position(), attach(), face_mask(), edge_mask(), face_profile(), edge_profile(), corner_profile()
1854// Usage:
1855// PARENT() corner_mask([corners], [except]) CHILDREN;
1856// Description:
1857// Takes a 3D mask shape, and attaches it to the specified corners, with the appropriate orientation to
1858// be differenced away. The 3D corner mask shape should be designed to mask away the X+Y+Z+ octant. If no tag is set
1859// then `corner_mask` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1860// See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
1861// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1862// Arguments:
1863// corners = Corners to mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: All corners.
1864// except = Corners to explicitly NOT mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: No corners.
1865// Side Effects:
1866// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1867// `$idx` is set to the index number of each corner.
1868// `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1869// Example:
1870// diff()
1871// cube(100, center=true)
1872// corner_mask([TOP,FRONT],LEFT+FRONT+TOP)
1873// difference() {
1874// translate(-0.01*[1,1,1]) cube(20);
1875// translate([20,20,20]) sphere(r=20);
1876// }
1877module corner_mask(corners=CORNERS_ALL, except=[]) {
1878 req_children($children);
1879 assert($parent_geom != undef, "No object to attach to!");
1880 corners = _corners(corners, except=except);
1881 vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
1882 for ($idx = idx(vecs)) {
1883 vec = vecs[$idx];
1884 vcount = (vec.x?1:0) + (vec.y?1:0) + (vec.z?1:0);
1885 dummy=assert(vcount == 3, "Not an edge vector!");
1886 anch = _find_anchor(vec, $parent_geom);
1887 $attach_to = undef;
1888 $attach_anchor = anch;
1889 rotang = vec.z<0?
1890 [ 0,0,180+v_theta(vec)-45] :
1891 [180,0,-90+v_theta(vec)-45];
1892 translate(anch[1]) rot(rotang)
1893 default_tag("remove") children();
1894 }
1895}
1896
1897
1898// Module: face_profile()
1899// Synopsis: Extrudes a 2D edge profile into a mask for all edges and corners of the given faces on the parent.
1900// SynTags: Geom
1901// Topics: Attachments, Masking
1902// See Also: attachable(), position(), attach(), edge_profile(), corner_profile(), face_mask(), edge_mask(), corner_mask()
1903// Usage:
1904// PARENT() face_profile(faces, r|d=, [convexity=]) CHILDREN;
1905// Description:
1906// Given a 2D edge profile, extrudes it into a mask for all edges and corners bounding each given face. If no tag is set
1907// then `face_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1908// See [Specifying Faces](attachments.scad#subsection-specifying-faces) for information on specifying faces.
1909// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1910// Arguments:
1911// faces = Faces to mask edges and corners of.
1912// r = Radius of corner mask.
1913// ---
1914// d = Diameter of corner mask.
1915// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
1916// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
1917// Side Effects:
1918// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1919// `$idx` is set to the index number of each face.
1920// `$attach_anchor` is set for each edge or corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1921// `$profile_type` is set to `"edge"` or `"corner"`, depending on what is being masked.
1922// Example:
1923// diff()
1924// cube([50,60,70],center=true)
1925// face_profile(TOP,r=10)
1926// mask2d_roundover(r=10);
1927module face_profile(faces=[], r, d, excess=0.01, convexity=10) {
1928 req_children($children);
1929 faces = is_vector(faces)? [faces] : faces;
1930 assert(all([for (face=faces) is_vector(face) && sum([for (x=face) x!=0? 1 : 0])==1]), "Vector in faces doesn't point at a face.");
1931 r = get_radius(r=r, d=d, dflt=undef);
1932 assert(is_num(r) && r>=0);
1933 edge_profile(faces, excess=excess) children();
1934 corner_profile(faces, convexity=convexity, r=r) children();
1935}
1936
1937
1938// Module: edge_profile()
1939// Synopsis: Extrudes a 2d edge profile into a mask on the given edges of the parent.
1940// SynTags: Geom
1941// Topics: Attachments, Masking
1942// See Also: attachable(), position(), attach(), face_profile(), edge_profile_asym(), corner_profile(), edge_mask(), face_mask(), corner_mask()
1943// Usage:
1944// PARENT() edge_profile([edges], [except], [convexity]) CHILDREN;
1945// Description:
1946// Takes a 2D mask shape and attaches it to the selected edges, with the appropriate orientation and
1947// extruded length to be `diff()`ed away, to give the edge a matching profile. If no tag is set
1948// then `edge_profile` sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
1949// For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
1950// For a step-by-step
1951// explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
1952// Arguments:
1953// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
1954// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
1955// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
1956// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
1957// Side Effects:
1958// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
1959// `$idx` is set to the index number of each edge.
1960// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
1961// `$profile_type` is set to `"edge"`.
1962// `$edge_angle` is set to the inner angle of the current edge.
1963// Example:
1964// diff()
1965// cube([50,60,70],center=true)
1966// edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
1967// mask2d_roundover(r=10, inset=2);
1968// Example: Using $edge_angle on a Conoid
1969// diff()
1970// cyl(d1=50, d2=30, l=40, anchor=BOT) {
1971// edge_profile([TOP,BOT], excess=10, convexity=6) {
1972// mask2d_roundover(r=8, inset=1, excess=1, mask_angle=$edge_angle);
1973// }
1974// }
1975// Example: Using $edge_angle on a Prismoid
1976// diff()
1977// prismoid([60,50],[30,20],h=40,shift=[-25,15]) {
1978// edge_profile(excess=10, convexity=20) {
1979// mask2d_roundover(r=5,inset=1,mask_angle=$edge_angle);
1980// }
1981// }
1982
1983module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) {
1984 req_children($children);
1985 check1 = assert($parent_geom != undef, "No object to attach to!");
1986 conoid = $parent_geom[0] == "conoid";
1987 edges = !conoid? _edges(edges, except=except) :
1988 edges==EDGES_ALL? [TOP,BOT] :
1989 assert(all([for (e=edges) in_list(e,[TOP,BOT])]), "Invalid conoid edge spec.")
1990 edges;
1991 vecs = conoid
1992 ? [for (e=edges) e+FWD]
1993 : [
1994 for (i = [0:3], axis=[0:2])
1995 if (edges[axis][i]>0)
1996 EDGE_OFFSETS[axis][i]
1997 ];
1998 all_vecs_are_edges = all([for (vec = vecs) sum(v_abs(vec))==2]);
1999 check2 = assert(all_vecs_are_edges, "All vectors must be edges.");
2000 default_tag("remove")
2001 for ($idx = idx(vecs)) {
2002 vec = vecs[$idx];
2003 anch = _find_anchor(vec, $parent_geom);
2004 path_angs_T = _attach_geom_edge_path($parent_geom, vec);
2005 path = path_angs_T[0];
2006 vecs = path_angs_T[1];
2007 post_T = path_angs_T[2];
2008 $attach_to = undef;
2009 $attach_anchor = anch;
2010 $profile_type = "edge";
2011 multmatrix(post_T) {
2012 for (i = idx(path,e=-2)) {
2013 pt1 = select(path,i);
2014 pt2 = select(path,i+1);
2015 cp = (pt1 + pt2) / 2;
2016 v1 = vecs[i][0];
2017 v2 = vecs[i][1];
2018 $edge_angle = 180 - vector_angle(v1,v2);
2019 if (!approx(pt1,pt2)) {
2020 seglen = norm(pt2-pt1) + 2 * excess;
2021 move(cp) {
2022 frame_map(x=-v2, z=unit(pt2-pt1)) {
2023 linear_extrude(height=seglen, center=true, convexity=convexity)
2024 mirror([-1,1]) children();
2025 }
2026 }
2027 }
2028 }
2029 }
2030 }
2031}
2032
2033
2034// Module: edge_profile_asym()
2035// Synopsis: Extrudes an asymmetric 2D profile into a mask on the given edges and corners of the parent.
2036// SynTags: Geom
2037// Topics: Attachments, Masking
2038// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_profile(), edge_mask(), face_mask(), corner_mask()
2039// Usage:
2040// PARENT() edge_profile([edges], [except=], [convexity=], [flip=], [corner_type=]) CHILDREN;
2041// Description:
2042// Takes an asymmetric 2D mask shape and attaches it to the selected edges and corners, with the appropriate
2043// orientation and extruded length to be `diff()`ed away, to give the edges and corners a matching profile.
2044// If no tag is set then `edge_profile_asym()` sets the tag for children to "remove" so that it will work
2045// with the default {{diff()}} tag. For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
2046// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2047// .
2048// The asymmetric profiles are joined consistently at the corners. This is impossible if all three edges at a corner use the profile, hence
2049// this situation is not permitted. The profile orientation can be inverted using the `flip=true` parameter.
2050// .
2051// The standard profiles are located in the first quadrant and have positive X values. If you provide a profile located in the second quadrant,
2052// where the X values are negative, then it will produce a fillet. You can flip any of the standard profiles using {{xflip()}}.
2053// Fillets are always asymmetric because at a given edge, they can blend in two different directions, so even for symmetric profiles,
2054// the asymmetric logic is required. You can set the `corner_type` parameter to select rounded, chamfered or sharp corners.
2055// However, when the corners are inside (concave) corners, you must provide the size of the profile ([width,height]), because the
2056// this information is required to produce the correct corner and cannot be obtain from the profile itself, which is a child object.
2057// Arguments:
2058// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
2059// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
2060// excess = Excess length to extrude the profile to make edge masks. Default: 0.01
2061// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
2062// flip = If true, reverses the orientation of any external profile parts at each edge. Default false
2063// corner_type = Specifies how exterior corners should be formed. Must be one of `"none"`, `"chamfer"`, `"round"`, or `"sharp"`. Default: `"none"`
2064// size = If given the width and height of the 2D profile, will enable rounding and chamfering of internal corners when given a negative profile.
2065// Side Effects:
2066// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
2067// `$idx` is set to the index number of each edge.
2068// `$attach_anchor` is set for each edge given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
2069// `$profile_type` is set to `"edge"`.
2070// `$edge_angle` is set to the inner angle of the current edge.
2071// Example:
2072// ogee = [
2073// "xstep",1, "ystep",1, // Starting shoulder.
2074// "fillet",5, "round",5, // S-curve.
2075// "ystep",1, "xstep",1 // Ending shoulder.
2076// ];
2077// diff()
2078// cuboid(50) {
2079// edge_profile_asym(FRONT)
2080// mask2d_ogee(ogee);
2081// }
2082// Example: Flipped
2083// ogee = [
2084// "xstep",1, "ystep",1, // Starting shoulder.
2085// "fillet",5, "round",5, // S-curve.
2086// "ystep",1, "xstep",1 // Ending shoulder.
2087// ];
2088// diff()
2089// cuboid(50) {
2090// edge_profile_asym(FRONT, flip=true)
2091// mask2d_ogee(ogee);
2092// }
2093// Example: Negative Chamfering
2094// cuboid(50) {
2095// edge_profile_asym(FWD, flip=false)
2096// xflip() mask2d_chamfer(10);
2097// edge_profile_asym(BACK, flip=true, corner_type="sharp")
2098// xflip() mask2d_chamfer(10);
2099// }
2100// Example: Negative Roundings
2101// cuboid(50) {
2102// edge_profile_asym(FWD, flip=false)
2103// xflip() mask2d_roundover(10);
2104// edge_profile_asym(BACK, flip=true, corner_type="round")
2105// xflip() mask2d_roundover(10);
2106// }
2107// Example: Cornerless
2108// cuboid(50) {
2109// edge_profile_asym(
2110// "ALL", except=[TOP+FWD+RIGHT, BOT+BACK+LEFT]
2111// ) xflip() mask2d_roundover(10);
2112// }
2113// Example: More complicated edge sets
2114// cuboid(50) {
2115// edge_profile_asym(
2116// [FWD,BACK,BOT+RIGHT], except=[FWD+RIGHT,BOT+BACK],
2117// corner_type="round"
2118// ) xflip() mask2d_roundover(10);
2119// }
2120// Example: Mixing it up a bit.
2121// diff()
2122// cuboid(60) {
2123// tag("keep") edge_profile_asym(LEFT, flip=true, corner_type="chamfer")
2124// xflip() mask2d_chamfer(10);
2125// edge_profile_asym(RIGHT)
2126// mask2d_roundover(10);
2127// }
2128// Example: Chamfering internal corners.
2129// cuboid(40) {
2130// edge_profile_asym(
2131// [FWD+DOWN,FWD+LEFT],
2132// corner_type="chamfer", size=[10,10]/sqrt(2)
2133// ) xflip() mask2d_chamfer(10);
2134// }
2135// Example: Rounding internal corners.
2136// cuboid(40) {
2137// edge_profile_asym(
2138// [FWD+DOWN,FWD+LEFT],
2139// corner_type="round", size=[10,10]
2140// ) xflip() mask2d_roundover(10);
2141// }
2142
2143module edge_profile_asym(
2144 edges=EDGES_ALL, except=[],
2145 excess=0.01, convexity=10,
2146 flip=false, corner_type="none",
2147 size=[0,0]
2148) {
2149 function _corner_orientation(pos,pvec) =
2150 let(
2151 j = [for (i=[0:2]) if (pvec[i]) i][0],
2152 T = (pos.x>0? xflip() : ident(4)) *
2153 (pos.y>0? yflip() : ident(4)) *
2154 (pos.z>0? zflip() : ident(4)) *
2155 rot(-120*(2-j), v=[1,1,1])
2156 ) T;
2157
2158 function _default_edge_orientation(edge) =
2159 edge.z < 0? [[-edge.x,-edge.y,0], UP] :
2160 edge.z > 0? [[-edge.x,-edge.y,0], DOWN] :
2161 edge.y < 0? [[-edge.x,0,0], BACK] :
2162 [[-edge.x,0,0], FWD] ;
2163
2164 function _edge_transition_needs_flip(from,to) =
2165 let(
2166 flip_edges = [
2167 [BOT+FWD, [FWD+LEFT, FWD+RIGHT]],
2168 [BOT+BACK, [BACK+LEFT, BACK+RIGHT]],
2169 [BOT+LEFT, []],
2170 [BOT+RIGHT, []],
2171 [TOP+FWD, [FWD+LEFT, FWD+RIGHT]],
2172 [TOP+BACK, [BACK+LEFT, BACK+RIGHT]],
2173 [TOP+LEFT, []],
2174 [TOP+RIGHT, []],
2175 [FWD+LEFT, [TOP+FWD, BOT+FWD]],
2176 [FWD+RIGHT, [TOP+FWD, BOT+FWD]],
2177 [BACK+LEFT, [TOP+BACK, BOT+BACK]],
2178 [BACK+RIGHT, [TOP+BACK, BOT+BACK]],
2179 ],
2180 i = search([from], flip_edges, num_returns_per_match=1)[0],
2181 check = assert(i!=[], "Bad edge vector.")
2182 ) in_list(to,flip_edges[i][1]);
2183
2184 function _edge_corner_numbers(vec) =
2185 let(
2186 v2 = [for (i=idx(vec)) vec[i]? (vec[i]+1)/2*pow(2,i) : 0],
2187 off = v2.x + v2.y + v2.z,
2188 xs = [0, if (!vec.x) 1],
2189 ys = [0, if (!vec.y) 2],
2190 zs = [0, if (!vec.z) 4]
2191 ) [for (x=xs, y=ys, z=zs) x+y+z + off];
2192
2193 function _gather_contiguous_edges(edge_corners) =
2194 let(
2195 no_tri_corners = all([for(cn = [0:7]) len([for (ec=edge_corners) if(in_list(cn,ec[1])) 1])<3]),
2196 check = assert(no_tri_corners, "Cannot have three edges that meet at the same corner.")
2197 )
2198 _gather_contiguous_edges_r(
2199 [for (i=idx(edge_corners)) if(i) edge_corners[i]],
2200 edge_corners[0][1],
2201 [edge_corners[0][0]], []);
2202
2203 function _gather_contiguous_edges_r(edge_corners, ecns, curr, out) =
2204 len(edge_corners)==0? [each out, curr] :
2205 let(
2206 i1 = [
2207 for (i = idx(edge_corners))
2208 if (in_list(ecns[0], edge_corners[i][1]))
2209 i
2210 ],
2211 i2 = [
2212 for (i = idx(edge_corners))
2213 if (in_list(ecns[1], edge_corners[i][1]))
2214 i
2215 ]
2216 ) !i1 && !i2? _gather_contiguous_edges_r(
2217 [for (i=idx(edge_corners)) if(i) edge_corners[i]],
2218 edge_corners[0][1],
2219 [edge_corners[0][0]],
2220 [each out, curr]
2221 ) : let(
2222 nu_curr = [
2223 if (i1) edge_corners[i1[0]][0],
2224 each curr,
2225 if (i2) edge_corners[i2[0]][0],
2226 ],
2227 nu_ecns = [
2228 if (!i1) ecns[0] else [
2229 for (ecn = edge_corners[i1[0]][1])
2230 if (ecn != ecns[0]) ecn
2231 ][0],
2232 if (!i2) ecns[1] else [
2233 for (ecn = edge_corners[i2[0]][1])
2234 if (ecn != ecns[1]) ecn
2235 ][0],
2236 ],
2237 rem = [
2238 for (i = idx(edge_corners))
2239 if (i != i1[0] && i != i2[0])
2240 edge_corners[i]
2241 ]
2242 )
2243 _gather_contiguous_edges_r(rem, nu_ecns, nu_curr, out);
2244
2245 function _edge_transition_inversions(edge_string) =
2246 let(
2247 // boolean cumulative sum
2248 bcs = function(list, i=0, inv=false, out=[])
2249 i>=len(list)? out :
2250 let( nu_inv = list[i]? !inv : inv )
2251 bcs(list, i+1, nu_inv, [each out, nu_inv]),
2252 inverts = bcs([
2253 false,
2254 for(i = idx(edge_string)) if (i)
2255 _edge_transition_needs_flip(
2256 edge_string[i-1],
2257 edge_string[i]
2258 )
2259 ]),
2260 boti = [for(i = idx(edge_string)) if (edge_string[i].z<0) i],
2261 topi = [for(i = idx(edge_string)) if (edge_string[i].z>0) i],
2262 lfti = [for(i = idx(edge_string)) if (edge_string[i].x<0) i],
2263 rgti = [for(i = idx(edge_string)) if (edge_string[i].x>0) i],
2264 idx = [for (m = [boti, topi, lfti, rgti]) if(m) m[0]][0],
2265 rinverts = inverts[idx] == false? inverts : [for (x = inverts) !x]
2266 ) rinverts;
2267
2268 function _is_closed_edge_loop(edge_string) =
2269 let(
2270 e1 = edge_string[0],
2271 e2 = last(edge_string)
2272 )
2273 len([for (i=[0:2]) if (abs(e1[i])==1 && e1[i]==e2[i]) 1]) == 1 &&
2274 len([for (i=[0:2]) if (e1[i]==0 && abs(e2[i])==1) 1]) == 1 &&
2275 len([for (i=[0:2]) if (e2[i]==0 && abs(e1[i])==1) 1]) == 1;
2276
2277 function _edge_pair_perp_vec(e1,e2) =
2278 [for (i=[0:2]) if (abs(e1[i])==1 && e1[i]==e2[i]) -e1[i] else 0];
2279
2280 req_children($children);
2281 check1 = assert($parent_geom != undef, "No object to attach to!")
2282 assert(in_list(corner_type, ["none", "round", "chamfer", "sharp"]))
2283 assert(is_bool(flip));
2284 edges = _edges(edges, except=except);
2285 vecs = [
2286 for (i = [0:3], axis=[0:2])
2287 if (edges[axis][i]>0)
2288 EDGE_OFFSETS[axis][i]
2289 ];
2290 all_vecs_are_edges = all([for (vec = vecs) sum(v_abs(vec))==2]);
2291 check2 = assert(all_vecs_are_edges, "All vectors must be edges.");
2292 edge_corners = [for (vec = vecs) [vec, _edge_corner_numbers(vec)]];
2293 edge_strings = _gather_contiguous_edges(edge_corners);
2294 default_tag("remove")
2295 for (edge_string = edge_strings) {
2296 inverts = _edge_transition_inversions(edge_string);
2297 flipverts = [for (x = inverts) flip? !x : x];
2298 vecpairs = [
2299 for (i = idx(edge_string))
2300 let (p = _default_edge_orientation(edge_string[i]))
2301 flipverts[i]? [p.y,p.x] : p
2302 ];
2303 is_loop = _is_closed_edge_loop(edge_string);
2304 for (i = idx(edge_string)) {
2305 if (corner_type!="none" && (i || is_loop)) {
2306 e1 = select(edge_string,i-1);
2307 e2 = select(edge_string,i);
2308 vp1 = select(vecpairs,i-1);
2309 vp2 = select(vecpairs,i);
2310 pvec = _edge_pair_perp_vec(e1,e2);
2311 pos = [for (i=[0:2]) e1[i]? e1[i] : e2[i]];
2312 mirT = _corner_orientation(pos, pvec);
2313 $attach_to = undef;
2314 $attach_anchor = _find_anchor(pos, $parent_geom);
2315 $profile_type = "corner";
2316 position(pos) {
2317 multmatrix(mirT) {
2318 if (vp1.x == vp2.x && size.y > 0) {
2319 zflip() {
2320 if (corner_type=="chamfer") {
2321 fn = $fn;
2322 move([size.y,size.y]) {
2323 rotate_extrude(angle=90, $fn=4)
2324 left_half(planar=true, $fn=fn)
2325 zrot(-90) fwd(size.y) children();
2326 }
2327 linear_extrude(height=size.x) {
2328 mask2d_roundover(size.y, inset=0.01, $fn=4);
2329 }
2330 } else if (corner_type=="round") {
2331 move([size.y,size.y]) {
2332 rotate_extrude(angle=90)
2333 left_half(planar=true)
2334 zrot(-90) fwd(size.y) children();
2335 }
2336 linear_extrude(height=size.x) {
2337 mask2d_roundover(size.y, inset=0.01);
2338 }
2339 }
2340 }
2341 } else if (vp1.y == vp2.y) {
2342 if (corner_type=="chamfer") {
2343 fn = $fn;
2344 rotate_extrude(angle=90, $fn=4)
2345 right_half(planar=true, $fn=fn)
2346 children();
2347 rotate_extrude(angle=90, $fn=4)
2348 left_half(planar=true, $fn=fn)
2349 children();
2350 } else if (corner_type=="round") {
2351 rotate_extrude(angle=90)
2352 right_half(planar=true)
2353 children();
2354 rotate_extrude(angle=90)
2355 left_half(planar=true)
2356 children();
2357 } else { //corner_type == "sharp"
2358 intersection() {
2359 rot([90,0, 0]) linear_extrude(height=100,center=true,convexity=convexity) children();
2360 rot([90,0,90]) linear_extrude(height=100,center=true,convexity=convexity) children();
2361 }
2362 }
2363 }
2364 }
2365 }
2366 }
2367 }
2368 for (i = idx(edge_string)) {
2369 $attach_to = undef;
2370 $attach_anchor = _find_anchor(edge_string[i], $parent_geom);
2371 $profile_type = "edge";
2372 edge_profile(edge_string[i], excess=excess, convexity=convexity) {
2373 if (flipverts[i]) {
2374 mirror([-1,1]) children();
2375 } else {
2376 children();
2377 }
2378 }
2379 }
2380 }
2381}
2382
2383
2384
2385// Module: corner_profile()
2386// Synopsis: Rotationally extrudes a 2d edge profile into corner mask on the given corners of the parent.
2387// SynTags: Geom
2388// Topics: Attachments, Masking
2389// See Also: attachable(), position(), attach(), face_profile(), edge_profile(), corner_mask(), face_mask(), edge_mask()
2390// Usage:
2391// PARENT() corner_profile([corners], [except], [r=|d=], [convexity=]) CHILDREN;
2392// Description:
2393// Takes a 2D mask shape, rotationally extrudes and converts it into a corner mask, and attaches it
2394// to the selected corners with the appropriate orientation. If no tag is set then `corner_profile()`
2395// sets the tag for children to "remove" so that it will work with the default {{diff()}} tag.
2396// See [Specifying Corners](attachments.scad#subsection-specifying-corners) for information on how to specify corner sets.
2397// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2398// Arguments:
2399// corners = Corners to mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: All corners.
2400// except = Corners to explicitly NOT mask. See [Specifying Corners](attachments.scad#subsection-specifying-corners). Default: No corners.
2401// ---
2402// r = Radius of corner mask.
2403// d = Diameter of corner mask.
2404// convexity = Max number of times a line could intersect the perimeter of the mask shape. Default: 10
2405// Side Effects:
2406// Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
2407// `$idx` is set to the index number of each corner.
2408// `$attach_anchor` is set for each corner given, to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor.
2409// `$profile_type` is set to `"corner"`.
2410// Example:
2411// diff()
2412// cuboid([50,60,70],rounding=10,edges="Z",anchor=CENTER) {
2413// corner_profile(TOP,r=10)
2414// mask2d_teardrop(r=10, angle=40);
2415// }
2416module corner_profile(corners=CORNERS_ALL, except=[], r, d, convexity=10) {
2417 check1 = assert($parent_geom != undef, "No object to attach to!");
2418 r = max(0.01, get_radius(r=r, d=d, dflt=undef));
2419 check2 = assert(is_num(r), "Bad r/d argument.");
2420 corners = _corners(corners, except=except);
2421 vecs = [for (i = [0:7]) if (corners[i]>0) CORNER_OFFSETS[i]];
2422 all_vecs_are_corners = all([for (vec = vecs) sum(v_abs(vec))==3]);
2423 check3 = assert(all_vecs_are_corners, "All vectors must be corners.");
2424 for ($idx = idx(vecs)) {
2425 vec = vecs[$idx];
2426 anch = _find_anchor(vec, $parent_geom);
2427 $attach_to = undef;
2428 $attach_anchor = anch;
2429 $profile_type = "corner";
2430 rotang = vec.z<0?
2431 [ 0,0,180+v_theta(vec)-45] :
2432 [180,0,-90+v_theta(vec)-45];
2433 default_tag("remove"){
2434 translate(anch[1]) {
2435 rot(rotang) {
2436 down(0.01) {
2437 linear_extrude(height=r+0.01, center=false) {
2438 difference() {
2439 translate(-[0.01,0.01]) square(r);
2440 translate([r,r]) circle(r=r*0.999);
2441 }
2442 }
2443 }
2444 translate([r,r]) zrot(180) {
2445 rotate_extrude(angle=90, convexity=convexity) {
2446 right(r) xflip() {
2447 children();
2448 }
2449 }
2450 }
2451 }
2452 }
2453 }
2454 }
2455}
2456
2457
2458// Section: Making your objects attachable
2459
2460
2461// Module: attachable()
2462// Synopsis: Manages the anchoring, spin, orientation, and attachments for an object.
2463// Topics: Attachments
2464// See Also: reorient()
2465// Usage: Square/Trapezoid Geometry
2466// attachable(anchor, spin, two_d=true, size=, [size2=], [shift=], [override=], ...) {OBJECT; children();}
2467// Usage: Circle/Oval Geometry
2468// attachable(anchor, spin, two_d=true, r=|d=, ...) {OBJECT; children();}
2469// Usage: 2D Path/Polygon Geometry
2470// attachable(anchor, spin, two_d=true, path=, [extent=], ...) {OBJECT; children();}
2471// Usage: 2D Region Geometry
2472// attachable(anchor, spin, two_d=true, region=, [extent=], ...) {OBJECT; children();}
2473// Usage: Cubical/Prismoidal Geometry
2474// attachable(anchor, spin, [orient], size=, [size2=], [shift=], [override=], ...) {OBJECT; children();}
2475// Usage: Cylindrical Geometry
2476// attachable(anchor, spin, [orient], r=|d=, l=, [axis=], ...) {OBJECT; children();}
2477// Usage: Conical Geometry
2478// attachable(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], ...) {OBJECT; children();}
2479// Usage: Spheroid/Ovoid Geometry
2480// attachable(anchor, spin, [orient], r=|d=, ...) {OBJECT; children();}
2481// Usage: Extruded Path/Polygon Geometry
2482// attachable(anchor, spin, path=, l=|h=, [extent=], ...) {OBJECT; children();}
2483// Usage: Extruded Region Geometry
2484// attachable(anchor, spin, region=, l=|h=, [extent=], ...) {OBJECT; children();}
2485// Usage: VNF Geometry
2486// attachable(anchor, spin, [orient], vnf=, [extent=], ...) {OBJECT; children();}
2487// Usage: Pre-Specified Geometry
2488// attachable(anchor, spin, [orient], geom=) {OBJECT; children();}
2489//
2490// Description:
2491// Manages the anchoring, spin, orientation, and attachments for OBJECT, located in a 3D volume or 2D area.
2492// A managed 3D volume is assumed to be vertically (Z-axis) oriented, and centered.
2493// A managed 2D area is just assumed to be centered. The shape to be managed is given
2494// as the first child to this module, and the second child should be given as `children()`.
2495// For example, to manage a conical shape:
2496// ```openscad
2497// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h) {
2498// cyl(r1=r1, r2=r2, l=h);
2499// children();
2500// }
2501// ```
2502// .
2503// If this is *not* run as a child of `attach()` with the `to` argument
2504// given, then the following transformations are performed in order:
2505// * Translates so the `anchor` point is at the origin (0,0,0).
2506// * Rotates around the Z axis by `spin` degrees counter-clockwise.
2507// * Rotates so the top of the part points towards the vector `orient`.
2508// .
2509// If this is called as a child of `attach(from,to)`, then the info
2510// for the anchor points referred to by `from` and `to` are fetched,
2511// which will include position, direction, and spin. With that info,
2512// the following transformations are performed:
2513// * Translates this part so it's anchor position matches the parent's anchor position.
2514// * Rotates this part so it's anchor direction vector exactly opposes the parent's anchor direction vector.
2515// * Rotates this part so it's anchor spin matches the parent's anchor spin.
2516// .
2517// In addition to handling positioning of the attachable object,
2518// this module is also responsible for handing coloring of objects with {{recolor()}} and {{color_this()}}, and
2519// it is responsible for processing tags and determining whether the object should
2520// display or not in the current context. The determination based on the tags of whether to display the attachable object
2521// often occurs in this module, which means that an object which does not display (e.g. a "remove" tagged object
2522// inside {{diff()}}) cannot have internal {{tag()}} calls that change its tags and cause submodel
2523// portions to display: the entire object simply does not run. If you want the use the attachable object's internal tags outside
2524// of the attachable object you can set `expose_tags=true` which delays the determination to display objects to the children.
2525// For this to work correctly, all of the children must be attachables. An example situation where you should set
2526// `expose_tags=true` is when you want to have negative space in an attachable object that gets removed from the parent via
2527// a "remove" tagged component of your attachable.
2528// .
2529// Application of {{recolor()}} and {{color_this()}} also happens in this module and normally it applies to the
2530// entire attachable object, so coloring commands that you give internally in the first child to `attachable()` have no effect.
2531// Generally it makes sense that if a user specifies a color for an attachable object, the entire object is displayed
2532// in that color, but if you want to retain control of color for sub-parts of an attachable object, you can use
2533// the `keep_color=true` option, which delays the assignment of colors to the child level. For this to work
2534// correctly, all of the sub-parts of your attachable object must be attachables. Also note that this option could
2535// be confusing to users who don't understand why color commands are not working on the object.
2536// .
2537// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2538//
2539// Arguments:
2540// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
2541// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
2542// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
2543// ---
2544// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
2545// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
2546// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
2547// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2548// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2549// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
2550// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
2551// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
2552// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
2553// l/h = Length of the cylindrical, conical, or extruded path volume along axis.
2554// vnf = The [VNF](vnf.scad) of the volume.
2555// path = The path to generate a polygon from.
2556// region = The region to generate a shape from.
2557// extent = If true, calculate anchors by extents, rather than intersection, for VNFs and paths. Default: true.
2558// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
2559// offset = If given, offsets the perimeter of the volume around the centerpoint.
2560// anchors = If given as a list of anchor points, allows named anchor points.
2561// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
2562// axis = The vector pointing along the axis of a geometry. Default: UP
2563// override = Function that takes an anchor and for 3d returns a triple `[position, direction, spin]` or for 2d returns a pair `[position,direction]` to use for that anchor to override the normal one. You can also supply a lookup table that is a list of `[anchor, [position, direction, spin]]` entries. If the direction/position/spin that is returned is undef then the default will be used. This option applies only to the "trapezoid" and "prismoid" geometry types.
2564// geom = If given, uses the pre-defined (via {{attach_geom()}} geometry.
2565// expose_tags = If true then delay the decision to display or not display this object to the children, which it possible for tags to respond to operations like {{diff()}} used outside the attachble object. Only works correctly if everything in the attachable is also attachable. Default: false
2566// keep_color = If true then delay application of color to the children, which means that externally applied color is overridden by color specified within the attachable. Only works properly if everything in the attachable is also attacahble. Default: false
2567//
2568// Side Effects:
2569// `$parent_anchor` is set to the parent object's `anchor` value.
2570// `$parent_spin` is set to the parent object's `spin` value.
2571// `$parent_orient` is set to the parent object's `orient` value.
2572// `$parent_geom` is set to the parent object's `geom` value.
2573// `$parent_size` is set to the parent object's cubical `[X,Y,Z]` volume size.
2574// `$color` is used to set the color of the object
2575// `$save_color` is used to revert color to the parent's color
2576//
2577// Example(NORENDER): Cubical Shape
2578// attachable(anchor, spin, orient, size=size) {
2579// cube(size, center=true);
2580// children();
2581// }
2582//
2583// Example(NORENDER): Prismoidal Shape
2584// attachable(
2585// anchor, spin, orient,
2586// size=point3d(botsize,h),
2587// size2=topsize,
2588// shift=shift
2589// ) {
2590// prismoid(botsize, topsize, h=h, shift=shift);
2591// children();
2592// }
2593//
2594// Example(NORENDER): Cylindrical Shape, Z-Axis Aligned
2595// attachable(anchor, spin, orient, r=r, l=h) {
2596// cyl(r=r, l=h);
2597// children();
2598// }
2599//
2600// Example(NORENDER): Cylindrical Shape, Y-Axis Aligned
2601// attachable(anchor, spin, orient, r=r, l=h, axis=BACK) {
2602// cyl(r=r, l=h);
2603// children();
2604// }
2605//
2606// Example(NORENDER): Cylindrical Shape, X-Axis Aligned
2607// attachable(anchor, spin, orient, r=r, l=h, axis=RIGHT) {
2608// cyl(r=r, l=h);
2609// children();
2610// }
2611//
2612// Example(NORENDER): Conical Shape, Z-Axis Aligned
2613// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h) {
2614// cyl(r1=r1, r2=r2, l=h);
2615// children();
2616// }
2617//
2618// Example(NORENDER): Conical Shape, Y-Axis Aligned
2619// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h, axis=BACK) {
2620// cyl(r1=r1, r2=r2, l=h);
2621// children();
2622// }
2623//
2624// Example(NORENDER): Conical Shape, X-Axis Aligned
2625// attachable(anchor, spin, orient, r1=r1, r2=r2, l=h, axis=RIGHT) {
2626// cyl(r1=r1, r2=r2, l=h);
2627// children();
2628// }
2629//
2630// Example(NORENDER): Spherical Shape
2631// attachable(anchor, spin, orient, r=r) {
2632// sphere(r=r);
2633// children();
2634// }
2635//
2636// Example(NORENDER): Extruded Polygon Shape, by Extents
2637// attachable(anchor, spin, orient, path=path, l=length) {
2638// linear_extrude(height=length, center=true)
2639// polygon(path);
2640// children();
2641// }
2642//
2643// Example(NORENDER): Extruded Polygon Shape, by Intersection
2644// attachable(anchor, spin, orient, path=path, l=length, extent=false) {
2645// linear_extrude(height=length, center=true)
2646// polygon(path);
2647// children();
2648// }
2649//
2650// Example(NORENDER): Arbitrary VNF Shape, by Extents
2651// attachable(anchor, spin, orient, vnf=vnf) {
2652// vnf_polyhedron(vnf);
2653// children();
2654// }
2655//
2656// Example(NORENDER): Arbitrary VNF Shape, by Intersection
2657// attachable(anchor, spin, orient, vnf=vnf, extent=false) {
2658// vnf_polyhedron(vnf);
2659// children();
2660// }
2661//
2662// Example(NORENDER): 2D Rectangular Shape
2663// attachable(anchor, spin, orient, two_d=true, size=size) {
2664// square(size, center=true);
2665// children();
2666// }
2667//
2668// Example(NORENDER): 2D Trapezoidal Shape
2669// attachable(
2670// anchor, spin, orient,
2671// two_d=true,
2672// size=[x1,y],
2673// size2=x2,
2674// shift=shift
2675// ) {
2676// trapezoid(w1=x1, w2=x2, h=y, shift=shift);
2677// children();
2678// }
2679//
2680// Example(NORENDER): 2D Circular Shape
2681// attachable(anchor, spin, orient, two_d=true, r=r) {
2682// circle(r=r);
2683// children();
2684// }
2685//
2686// Example(NORENDER): Arbitrary 2D Polygon Shape, by Extents
2687// attachable(anchor, spin, orient, two_d=true, path=path) {
2688// polygon(path);
2689// children();
2690// }
2691//
2692// Example(NORENDER): Arbitrary 2D Polygon Shape, by Intersection
2693// attachable(anchor, spin, orient, two_d=true, path=path, extent=false) {
2694// polygon(path);
2695// children();
2696// }
2697//
2698// Example(NORENDER): Using Pre-defined Geometry
2699// geom = atype=="perim"? attach_geom(two_d=true, path=path, extent=false) :
2700// atype=="extents"? attach_geom(two_d=true, path=path, extent=true) :
2701// atype=="circle"? attach_geom(two_d=true, r=r) :
2702// assert(false, "Bad atype");
2703// attachable(anchor, spin, orient, geom=geom) {
2704// polygon(path);
2705// children();
2706// }
2707//
2708// Example: An object can be designed to attach as negative space using {{diff()}}, but if you want an object to include both positive and negative space then you run into trouble because tags inside the `attachable()` are ignored. One solution is to call attachable() twice. This example shows how two calls to attachable can create an object with positive and negative space. Note, however, that children in the negative space are differenced away: the highlighted little cube does not survive into the final model.
2709// module thing(anchor,spin,orient) {
2710// tag("remove") attachable(size=[15,15,15],anchor=anchor,spin=spin,orient=orient){
2711// cuboid([10,10,16]);
2712// union(){} // dummy children
2713// }
2714// attachable(size=[15,15,15], anchor=anchor, spin=spin, orient=orient){
2715// cuboid([15,15,15]);
2716// children();
2717// }
2718// }
2719// diff()
2720// cube([19,10,19])
2721// attach([FRONT],overlap=-4)
2722// thing(anchor=TOP)
2723// # attach(TOP) cuboid(2,anchor=TOP);
2724// Example: Here is an example where the "keep" tag allows children to appear in the negative space. That tag is also needed for this module to produce the desired output. As above, the tag must be applied outside the attachable() call.
2725// module thing(anchor = CENTER, spin = 0, orient = UP) {
2726// tag("remove") attachable(anchor, spin, orient, d1=0,d2=95,h=33) {
2727// cylinder(h = 33.1, d1 = 0, d2 = 95, anchor=CENTER);
2728// union(){} // dummy children
2729// }
2730// tag("keep") attachable(anchor, spin, orient,d1=0,d2=95,h=33) {
2731// cylinder(h = 33, d = 10,anchor=CENTER);
2732// children();
2733// }
2734// }
2735// diff()
2736// cube(100)
2737// attach([FRONT,TOP],overlap=-4)
2738// thing(anchor=TOP)
2739// tube(ir=12,h=10);
2740// Example: A different way to achieve similar effects to the above to examples is to use the `expose_tags` parameter. This parameter allows you to use just one call to attachable. The second example above can also be rewritten like this.
2741// module thing(anchor,spin,orient) {
2742// attachable(size=[15,15,15],anchor=anchor,spin=spin,orient=orient,expose_tags=true){
2743// union(){
2744// cuboid([15,15,15]);
2745// tag("remove")cuboid([10,10,16]);
2746// }
2747// children();
2748// }
2749// }
2750// diff()
2751// cube([19,10,19])
2752// attach([FRONT],overlap=-4)
2753// thing(anchor=TOP);
2754// Example: An advantage of using `expose_tags` is that it can work on nested constructions. Here the child cylinder is aligned relative to its parent and removed from the calling parent object.
2755// $fn=64;
2756// module thing(anchor=BOT){
2757// attachable(anchor = anchor,d=9,h=6,expose_tags=true){
2758// cyl(d = 9, h = 6)
2759// tag("remove")
2760// align(RIGHT+TOP,inside=true)
2761// left(1)up(1)cyl(l=11, d=3);
2762// children();
2763// }
2764// }
2765// back_half()
2766// diff()
2767// cuboid(10)
2768// position(TOP)thing(anchor=BOT);
2769// Example(3D,NoAxes): Here an attachable module uses {{recolor()}} to change the color of a sub-part, producing the result shown on the left. But if the caller applies color to the attachable, then both the green and yellow are changed, as shown on the right.
2770// module thing(anchor=CENTER) {
2771// attachable(anchor,size=[10,10,10]) {
2772// cuboid(10)
2773// position(TOP) recolor("green")
2774// cuboid(5,anchor=BOT);
2775// children();
2776// }
2777// }
2778// move([-15,-15])
2779// thing()
2780// attach(RIGHT,BOT)
2781// recolor("blue") cyl(d=5,h=5);
2782// recolor("pink") thing()
2783// attach(RIGHT,BOT)
2784// recolor("blue") cyl(d=5,h=5);
2785// Example(3D,NoAxes): Using the `keep_color=true` option enables the green color to persist, even when the user specifies a color.
2786// module thing(anchor=CENTER) {
2787// attachable(anchor,size=[10,10,10],keep_color=true) {
2788// cuboid(10)
2789// position(TOP) recolor("green")
2790// cuboid(5,anchor=BOT);
2791// children();
2792// }
2793// }
2794// recolor("pink") thing()
2795// attach(RIGHT,BOT)
2796// recolor("blue") cyl(d=5,h=5);
2797
2798module attachable(
2799 anchor, spin, orient,
2800 size, size2, shift,
2801 r,r1,r2, d,d1,d2, l,h,
2802 vnf, path, region,
2803 extent=true,
2804 cp=[0,0,0],
2805 offset=[0,0,0],
2806 anchors=[],
2807 two_d=false,
2808 axis=UP,override,
2809 geom,
2810 expose_tags=false, keep_color=false
2811) {
2812 dummy1 =
2813 assert($children==2, "attachable() expects exactly two children; the shape to manage, and the union of all attachment candidates.")
2814 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
2815 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
2816 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient));
2817 anchor = first_defined([anchor, CENTER]);
2818 spin = default(spin, 0);
2819 orient = is_def($anchor_override)? UP : default(orient, UP);
2820 region = !is_undef(region)? region :
2821 !is_undef(path)? [path] :
2822 undef;
2823 geom = is_def(geom)? geom :
2824 attach_geom(
2825 size=size, size2=size2, shift=shift,
2826 r=r, r1=r1, r2=r2, h=h,
2827 d=d, d1=d1, d2=d2, l=l,
2828 vnf=vnf, region=region, extent=extent,
2829 cp=cp, offset=offset, anchors=anchors,
2830 two_d=two_d, axis=axis, override=override
2831 );
2832 m = _attach_transform(anchor,spin,orient,geom);
2833 multmatrix(m) {
2834 $parent_anchor = anchor;
2835 $parent_spin = spin;
2836 $parent_orient = orient;
2837 $parent_geom = geom;
2838 $parent_size = _attach_geom_size(geom);
2839 $attach_to = undef;
2840 $anchor_override=undef;
2841 $attach_alignment=undef;
2842 if (expose_tags || _is_shown()){
2843 if (!keep_color)
2844 _color($color) children(0);
2845 else {
2846 $save_color=undef; // Force color_this() color in effect to persist for the entire object
2847 children(0);
2848 }
2849 }
2850 if (is_def($save_color)) {
2851 $color=$save_color; // Revert to the color before color_this() call
2852 $save_color=undef;
2853 children(1);
2854 }
2855 else children(1);
2856 }
2857}
2858
2859// Function: reorient()
2860// Synopsis: Calculates the transformation matrix needed to reorient an object.
2861// SynTags: Trans, Path, VNF
2862// Topics: Attachments
2863// See Also: reorient(), attachable()
2864// Usage: Square/Trapezoid Geometry
2865// mat = reorient(anchor, spin, [orient], two_d=true, size=, [size2=], [shift=], ...);
2866// pts = reorient(anchor, spin, [orient], two_d=true, size=, [size2=], [shift=], p=, ...);
2867// Usage: Circle/Oval Geometry
2868// mat = reorient(anchor, spin, [orient], two_d=true, r=|d=, ...);
2869// pts = reorient(anchor, spin, [orient], two_d=true, r=|d=, p=, ...);
2870// Usage: 2D Path/Polygon Geometry
2871// mat = reorient(anchor, spin, [orient], two_d=true, path=, [extent=], ...);
2872// pts = reorient(anchor, spin, [orient], two_d=true, path=, [extent=], p=, ...);
2873// Usage: 2D Region/Polygon Geometry
2874// mat = reorient(anchor, spin, [orient], two_d=true, region=, [extent=], ...);
2875// pts = reorient(anchor, spin, [orient], two_d=true, region=, [extent=], p=, ...);
2876// Usage: Cubical/Prismoidal Geometry
2877// mat = reorient(anchor, spin, [orient], size=, [size2=], [shift=], ...);
2878// vnf = reorient(anchor, spin, [orient], size=, [size2=], [shift=], p=, ...);
2879// Usage: Cylindrical Geometry
2880// mat = reorient(anchor, spin, [orient], r=|d=, l=, [axis=], ...);
2881// vnf = reorient(anchor, spin, [orient], r=|d=, l=, [axis=], p=, ...);
2882// Usage: Conical Geometry
2883// mat = reorient(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], ...);
2884// vnf = reorient(anchor, spin, [orient], r1=|d1=, r2=|d2=, l=, [axis=], p=, ...);
2885// Usage: Spheroid/Ovoid Geometry
2886// mat = reorient(anchor, spin, [orient], r|d=, ...);
2887// vnf = reorient(anchor, spin, [orient], r|d=, p=, ...);
2888// Usage: Extruded Path/Polygon Geometry
2889// mat = reorient(anchor, spin, [orient], path=, l=|h=, [extent=], ...);
2890// vnf = reorient(anchor, spin, [orient], path=, l=|h=, [extent=], p=, ...);
2891// Usage: Extruded Region Geometry
2892// mat = reorient(anchor, spin, [orient], region=, l=|h=, [extent=], ...);
2893// vnf = reorient(anchor, spin, [orient], region=, l=|h=, [extent=], p=, ...);
2894// Usage: VNF Geometry
2895// mat = reorient(anchor, spin, [orient], vnf, [extent], ...);
2896// vnf = reorient(anchor, spin, [orient], vnf, [extent], p=, ...);
2897//
2898// Description:
2899// Given anchor, spin, orient, and general geometry info for a managed volume, this calculates
2900// the transformation matrix needed to be applied to the contents of that volume. A managed 3D
2901// volume is assumed to be vertically (Z-axis) oriented, and centered. A managed 2D area is just
2902// assumed to be centered.
2903// .
2904// If `p` is not given, then the transformation matrix will be returned.
2905// If `p` contains a VNF, a new VNF will be returned with the vertices transformed by the matrix.
2906// If `p` contains a path, a new path will be returned with the vertices transformed by the matrix.
2907// If `p` contains a point, a new point will be returned, transformed by the matrix.
2908// .
2909// If `$attach_to` is not defined, then the following transformations are performed in order:
2910// * Translates so the `anchor` point is at the origin (0,0,0).
2911// * Rotates around the Z axis by `spin` degrees counter-clockwise.
2912// * Rotates so the top of the part points towards the vector `orient`.
2913// .
2914// If `$attach_to` is defined, as a consequence of `attach(from,to)`, then
2915// the following transformations are performed in order:
2916// * Translates this part so it's anchor position matches the parent's anchor position.
2917// * Rotates this part so it's anchor direction vector exactly opposes the parent's anchor direction vector.
2918// * Rotates this part so it's anchor spin matches the parent's anchor spin.
2919// .
2920// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
2921//
2922// Arguments:
2923// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
2924// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
2925// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
2926// ---
2927// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
2928// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
2929// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
2930// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2931// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
2932// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
2933// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
2934// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
2935// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
2936// l/h = Length of the cylindrical, conical, or extruded path volume along axis.
2937// vnf = The [VNF](vnf.scad) of the volume.
2938// path = The path to generate a polygon from.
2939// region = The region to generate a shape from.
2940// extent = If true, calculate anchors by extents, rather than intersection. Default: false.
2941// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
2942// offset = If given, offsets the perimeter of the volume around the centerpoint.
2943// anchors = If given as a list of anchor points, allows named anchor points.
2944// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
2945// axis = The vector pointing along the axis of a geometry. Default: UP
2946// p = The VNF, path, or point to transform.
2947function reorient(
2948 anchor, spin, orient,
2949 size, size2, shift,
2950 r,r1,r2, d,d1,d2, l,h,
2951 vnf, path, region,
2952 extent=true,
2953 offset=[0,0,0],
2954 cp=[0,0,0],
2955 anchors=[],
2956 two_d=false,
2957 axis=UP, override,
2958 geom,
2959 p=undef
2960) =
2961 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
2962 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
2963 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient))
2964 let(
2965 anchor = default(anchor, CENTER),
2966 spin = default(spin, 0),
2967 orient = default(orient, UP),
2968 region = !is_undef(region)? region :
2969 !is_undef(path)? [path] :
2970 undef,
2971 geom = is_def(geom)? geom :
2972 attach_geom(
2973 size=size, size2=size2, shift=shift,
2974 r=r, r1=r1, r2=r2, h=h,
2975 d=d, d1=d1, d2=d2, l=l,
2976 vnf=vnf, region=region, extent=extent,
2977 cp=cp, offset=offset, anchors=anchors,
2978 two_d=two_d, axis=axis, override=override
2979 ),
2980 $attach_to = undef,
2981 $anchor_override= undef,
2982 $attach_alignment = undef
2983 ) _attach_transform(anchor,spin,orient,geom,p);
2984
2985
2986// Function: named_anchor()
2987// Synopsis: Creates an anchor data structure.
2988// Topics: Attachments
2989// See Also: reorient(), attachable()
2990// Usage:
2991// a = named_anchor(name, pos, [orient], [spin]);
2992// a = named_anchor(name, [pos], rot=, [flip=]);
2993// Description:
2994// Creates an anchor data structure. You can specify the position, orient direction and spin directly.
2995// Alternatively for the 3D case you can give a 4x4 rotation matrix which can specify the orient and spin, and optionally
2996// the position, using a translation component of the matrix. If you specify `pos` along with `rot` then the position you
2997// give overrides any translation included in `rot`. For a step-by-step explanation of attachments,
2998// see the [Attachments Tutorial](Tutorial-Attachments).
2999// Arguments:
3000// name = The string name of the anchor. Lowercase. Words separated by single dashes. No spaces.
3001// pos = The [X,Y,Z] position of the anchor.
3002// orient = A vector pointing in the direction parts should project from the anchor position. Default: UP
3003// spin = If needed, the angle to rotate the part around the direction vector. Default: 0
3004// ---
3005// rot = A 4x4 rotations matrix, which may include a translation
3006// flip = If true, flip the anchor the opposite direction. Default: false
3007function named_anchor(name, pos, orient, spin, rot, flip) =
3008 assert(num_defined([orient,spin])==0 || num_defined([rot,flip])==0, "Cannot mix orient or spin with rot or flip")
3009 assert(num_defined([pos,rot])>0, "Must give pos or rot")
3010 is_undef(rot) ? [name, pos, default(orient,UP), default(spin,0)]
3011 :
3012 let(
3013 flip = default(flip,false),
3014 pos = default(pos,apply(rot,CTR)),
3015 rotpart = _force_rot(rot),
3016 dummy = assert(approx(det4(rotpart),1), "Input rotation is not a rotation matrix"),
3017 dir = flip ? apply(rotpart,DOWN)
3018 : apply(rotpart,UP),
3019 rot = flip? affine3d_rot_by_axis(apply(rotpart,BACK),180)*rot
3020 : rot,
3021 decode=rot_decode(rot(to=UP,from=dir)*_force_rot(rot)),
3022 spin = decode[0]*sign(decode[1].z)
3023 )
3024 [name, pos, dir, spin];
3025
3026
3027function _force_rot(T) =
3028 [for(i=[0:3])
3029 [for(j=[0:3]) j<3 ? T[i][j] :
3030 i==3 ? 1
3031 : 0]];
3032
3033// Function: attach_geom()
3034// Synopsis: Returns the internal geometry description of an attachable object.
3035// Topics: Attachments
3036// See Also: reorient(), attachable()
3037// Usage: Null/Point Geometry
3038// geom = attach_geom(...);
3039// Usage: Square/Trapezoid Geometry
3040// geom = attach_geom(two_d=true, size=, [size2=], [shift=], ...);
3041// Usage: Circle/Oval Geometry
3042// geom = attach_geom(two_d=true, r=|d=, ...);
3043// Usage: 2D Path/Polygon/Region Geometry
3044// geom = attach_geom(two_d=true, region=, [extent=], ...);
3045// Usage: Cubical/Prismoidal Geometry
3046// geom = attach_geom(size=, [size2=], [shift=], ...);
3047// Usage: Cylindrical Geometry
3048// geom = attach_geom(r=|d=, l=|h=, [axis=], ...);
3049// Usage: Conical Geometry
3050// geom = attach_geom(r1|d1=, r2=|d2=, l=, [axis=], ...);
3051// Usage: Spheroid/Ovoid Geometry
3052// geom = attach_geom(r=|d=, ...);
3053// Usage: Extruded 2D Path/Polygon/Region Geometry
3054// geom = attach_geom(region=, l=|h=, [extent=], [shift=], [scale=], [twist=], ...);
3055// Usage: VNF Geometry
3056// geom = attach_geom(vnf=, [extent=], ...);
3057//
3058// Description:
3059// Given arguments that describe the geometry of an attachable object, returns the internal geometry description.
3060// This will probably not not ever need to be called by the end user.
3061//
3062// Arguments:
3063// ---
3064// size = If given as a 3D vector, contains the XY size of the bottom of the cuboidal/prismoidal volume, and the Z height. If given as a 2D vector, contains the front X width of the rectangular/trapezoidal shape, and the Y length.
3065// size2 = If given as a 2D vector, contains the XY size of the top of the prismoidal volume. If given as a number, contains the back width of the trapezoidal shape.
3066// shift = If given as a 2D vector, shifts the top of the prismoidal or conical shape by the given amount. If given as a number, shifts the back of the trapezoidal shape right by that amount. Default: No shift.
3067// scale = If given as number or a 2D vector, scales the top of the shape, relative to the bottom. Default: `[1,1]`
3068// twist = If given as number, rotates the top of the shape by the given number of degrees clockwise, relative to the bottom. Default: `0`
3069// r = Radius of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
3070// d = Diameter of the cylindrical/conical volume. Can be a scalar, or a list of sizes per axis.
3071// r1 = Radius of the bottom of the conical volume. Can be a scalar, or a list of sizes per axis.
3072// r2 = Radius of the top of the conical volume. Can be a scalar, or a list of sizes per axis.
3073// d1 = Diameter of the bottom of the conical volume. Can be a scalar, a list of sizes per axis.
3074// d2 = Diameter of the top of the conical volume. Can be a scalar, a list of sizes per axis.
3075// l/h = Length of the cylindrical, conical or extruded region volume along axis.
3076// vnf = The [VNF](vnf.scad) of the volume.
3077// region = The region to generate a shape from.
3078// extent = If true, calculate anchors by extents, rather than intersection. Default: true.
3079// cp = If given, specifies the centerpoint of the volume. Default: `[0,0,0]`
3080// offset = If given, offsets the perimeter of the volume around the centerpoint.
3081// anchors = If given as a list of anchor points, allows named anchor points.
3082// two_d = If true, the attachable shape is 2D. If false, 3D. Default: false (3D)
3083// axis = The vector pointing along the axis of a geometry. Default: UP
3084// override = Function that takes an anchor and returns a pair `[position,direction]` to use for that anchor to override the normal one. You can also supply a lookup table that is a list of `[anchor, [position, direction]]` entries. If the direction/position that is returned is undef then the default will be used.
3085//
3086// Example(NORENDER): Null/Point Shape
3087// geom = attach_geom();
3088//
3089// Example(NORENDER): Cubical Shape
3090// geom = attach_geom(size=size);
3091//
3092// Example(NORENDER): Prismoidal Shape
3093// geom = attach_geom(
3094// size=point3d(botsize,h),
3095// size2=topsize, shift=shift
3096// );
3097//
3098// Example(NORENDER): Cylindrical Shape, Z-Axis Aligned
3099// geom = attach_geom(r=r, h=h);
3100//
3101// Example(NORENDER): Cylindrical Shape, Y-Axis Aligned
3102// geom = attach_geom(r=r, h=h, axis=BACK);
3103//
3104// Example(NORENDER): Cylindrical Shape, X-Axis Aligned
3105// geom = attach_geom(r=r, h=h, axis=RIGHT);
3106//
3107// Example(NORENDER): Conical Shape, Z-Axis Aligned
3108// geom = attach_geom(r1=r1, r2=r2, h=h);
3109//
3110// Example(NORENDER): Conical Shape, Y-Axis Aligned
3111// geom = attach_geom(r1=r1, r2=r2, h=h, axis=BACK);
3112//
3113// Example(NORENDER): Conical Shape, X-Axis Aligned
3114// geom = attach_geom(r1=r1, r2=r2, h=h, axis=RIGHT);
3115//
3116// Example(NORENDER): Spherical Shape
3117// geom = attach_geom(r=r);
3118//
3119// Example(NORENDER): Ovoid Shape
3120// geom = attach_geom(r=[r_x, r_y, r_z]);
3121//
3122// Example(NORENDER): Arbitrary VNF Shape, Anchored by Extents
3123// geom = attach_geom(vnf=vnf);
3124//
3125// Example(NORENDER): Arbitrary VNF Shape, Anchored by Intersection
3126// geom = attach_geom(vnf=vnf, extent=false);
3127//
3128// Example(NORENDER): 2D Rectangular Shape
3129// geom = attach_geom(two_d=true, size=size);
3130//
3131// Example(NORENDER): 2D Trapezoidal Shape
3132// geom = attach_geom(two_d=true, size=[x1,y], size2=x2, shift=shift, override=override);
3133//
3134// Example(NORENDER): 2D Circular Shape
3135// geom = attach_geom(two_d=true, r=r);
3136//
3137// Example(NORENDER): 2D Oval Shape
3138// geom = attach_geom(two_d=true, r=[r_x, r_y]);
3139//
3140// Example(NORENDER): Arbitrary 2D Region Shape, Anchored by Extents
3141// geom = attach_geom(two_d=true, region=region);
3142//
3143// Example(NORENDER): Arbitrary 2D Region Shape, Anchored by Intersection
3144// geom = attach_geom(two_d=true, region=region, extent=false);
3145//
3146// Example(NORENDER): Extruded Region, Anchored by Extents
3147// geom = attach_geom(region=region, l=height);
3148//
3149// Example(NORENDER): Extruded Region, Anchored by Intersection
3150// geom = attach_geom(region=region, l=length, extent=false);
3151//
3152
3153function _local_struct_val(struct, key)=
3154 assert(is_def(key),"key is missing")
3155 let(ind = search([key],struct)[0])
3156 ind == [] ? undef : struct[ind][1];
3157
3158
3159function attach_geom(
3160 size, size2,
3161 shift, scale, twist,
3162 r,r1,r2, d,d1,d2, l,h,
3163 vnf, region,
3164 extent=true,
3165 cp=[0,0,0],
3166 offset=[0,0,0],
3167 anchors=[],
3168 two_d=false,
3169 axis=UP, override
3170) =
3171 assert(is_bool(extent))
3172 assert(is_vector(cp) || is_string(cp))
3173 assert(is_vector(offset))
3174 assert(is_list(anchors))
3175 assert(is_bool(two_d))
3176 assert(is_vector(axis))
3177 !is_undef(size)? (
3178 let(
3179 over_f = is_undef(override) ? function(anchor) [undef,undef,undef]
3180 : is_func(override) ? override
3181 : function(anchor) _local_struct_val(override,anchor)
3182 )
3183 two_d? (
3184 let(
3185 size2 = default(size2, size.x),
3186 shift = default(shift, 0)
3187 )
3188 assert(is_vector(size,2))
3189 assert(is_num(size2))
3190 assert(is_num(shift))
3191 ["trapezoid", point2d(size), size2, shift, over_f, cp, offset, anchors]
3192 ) : (
3193 let(
3194 size2 = default(size2, point2d(size)),
3195 shift = default(shift, [0,0])
3196 )
3197 assert(is_vector(size,3))
3198 assert(is_vector(size2,2))
3199 assert(is_vector(shift,2))
3200 ["prismoid", size, size2, shift, axis, over_f, cp, offset, anchors]
3201 )
3202 ) : !is_undef(vnf)? (
3203 assert(is_vnf(vnf))
3204 assert(two_d == false)
3205 extent? ["vnf_extent", vnf, cp, offset, anchors] :
3206 ["vnf_isect", vnf, cp, offset, anchors]
3207 ) : !is_undef(region)? (
3208 assert(is_region(region),2)
3209 let( l = default(l, h) )
3210 two_d==true
3211 ? assert(is_undef(l))
3212 extent==true
3213 ? ["rgn_extent", region, cp, offset, anchors]
3214 : ["rgn_isect", region, cp, offset, anchors]
3215 : assert(is_finite(l))
3216 let(
3217 shift = default(shift, [0,0]),
3218 scale = is_num(scale)? [scale,scale] : default(scale, [1,1]),
3219 twist = default(twist, 0)
3220 )
3221 assert(is_vector(shift,2))
3222 assert(is_vector(scale,2))
3223 assert(is_num(twist))
3224 extent==true
3225 ? ["extrusion_extent", region, l, twist, scale, shift, cp, offset, anchors]
3226 : ["extrusion_isect", region, l, twist, scale, shift, cp, offset, anchors]
3227 ) :
3228 let(
3229 r1 = get_radius(r1=r1,d1=d1,r=r,d=d,dflt=undef)
3230 )
3231 !is_undef(r1)? (
3232 let( l = default(l, h) )
3233 !is_undef(l)? (
3234 let(
3235 shift = default(shift, [0,0]),
3236 r2 = get_radius(r1=r2,d1=d2,r=r,d=d,dflt=undef)
3237 )
3238 assert(is_num(r1) || is_vector(r1,2))
3239 assert(is_num(r2) || is_vector(r2,2))
3240 assert(is_num(l))
3241 assert(is_vector(shift,2))
3242 ["conoid", r1, r2, l, shift, axis, cp, offset, anchors]
3243 ) : (
3244 two_d? (
3245 assert(is_num(r1) || is_vector(r1,2))
3246 ["ellipse", r1, cp, offset, anchors]
3247 ) : (
3248 assert(is_num(r1) || is_vector(r1,3))
3249 ["spheroid", r1, cp, offset, anchors]
3250 )
3251 )
3252 ) :
3253 ["point", cp, offset, anchors];
3254
3255
3256
3257
3258
3259
3260//////////////////////////////////////////////////////////////////////////////////////////////////////////////
3261//
3262// Attachment internal functions
3263
3264
3265/// Internal Function: _attach_geom_2d()
3266/// Topics: Attachments
3267/// See Also: reorient(), attachable()
3268/// Usage:
3269/// bool = _attach_geom_2d(geom);
3270/// Description:
3271/// Returns true if the given attachment geometry description is for a 2D shape.
3272function _attach_geom_2d(geom) =
3273 let( type = geom[0] )
3274 type == "trapezoid" || type == "ellipse" ||
3275 type == "rgn_isect" || type == "rgn_extent";
3276
3277
3278/// Internal Function: _attach_geom_size()
3279/// Usage:
3280/// bounds = _attach_geom_size(geom);
3281/// Topics: Attachments
3282/// See Also: reorient(), attachable()
3283/// Description:
3284/// Returns the `[X,Y,Z]` bounding size for the given attachment geometry description.
3285function _attach_geom_size(geom) =
3286 let( type = geom[0] )
3287 type == "point"? [0,0,0] :
3288 type == "prismoid"? ( //size, size2, shift, axis
3289 let(
3290 size=geom[1], size2=geom[2], shift=point2d(geom[3]),
3291 maxx = max(size.x,size2.x),
3292 maxy = max(size.y,size2.y),
3293 z = size.z
3294 ) [maxx, maxy, z]
3295 ) : type == "conoid"? ( //r1, r2, l, shift
3296 let(
3297 r1=geom[1], r2=geom[2], l=geom[3],
3298 shift=point2d(geom[4]), axis=point3d(geom[5]),
3299 rx1 = default(r1[0],r1),
3300 ry1 = default(r1[1],r1),
3301 rx2 = default(r2[0],r2),
3302 ry2 = default(r2[1],r2),
3303 maxxr = max(rx1,rx2),
3304 maxyr = max(ry1,ry2)
3305 )
3306 approx(axis,UP)? [2*maxxr,2*maxyr,l] :
3307 approx(axis,RIGHT)? [l,2*maxyr,2*maxxr] :
3308 approx(axis,BACK)? [2*maxxr,l,2*maxyr] :
3309 [2*maxxr, 2*maxyr, l]
3310 ) : type == "spheroid"? ( //r
3311 let( r=geom[1] )
3312 is_num(r)? [2,2,2]*r : v_mul([2,2,2],point3d(r))
3313 ) : type == "vnf_extent" || type=="vnf_isect"? ( //vnf
3314 let(
3315 vnf = geom[1]
3316 ) vnf==EMPTY_VNF? [0,0,0] :
3317 let(
3318 mm = pointlist_bounds(geom[1][0]),
3319 delt = mm[1]-mm[0]
3320 ) delt
3321 ) : type == "extrusion_isect" || type == "extrusion_extent"? ( //path, l
3322 let(
3323 mm = pointlist_bounds(flatten(geom[1])),
3324 delt = mm[1]-mm[0]
3325 ) [delt.x, delt.y, geom[2]]
3326 ) : type == "trapezoid"? ( //size, size2
3327 let(
3328 size=geom[1], size2=geom[2], shift=geom[3],
3329 maxx = max(size.x,size2+abs(shift))
3330 ) [maxx, size.y]
3331 ) : type == "ellipse"? ( //r
3332 let( r=geom[1] )
3333 is_num(r)? [2,2]*r : v_mul([2,2],point2d(r))
3334 ) : type == "rgn_isect" || type == "rgn_extent"? ( //path
3335 let(
3336 mm = pointlist_bounds(flatten(geom[1])),
3337 delt = mm[1]-mm[0]
3338 ) [delt.x, delt.y]
3339 ) :
3340 assert(false, "Unknown attachment geometry type.");
3341
3342
3343
3344/// Internal Function: _attach_geom_edge_path()
3345/// Usage:
3346/// angle = _attach_geom_edge_path(geom, edge);
3347/// Topics: Attachments
3348/// See Also: reorient(), attachable()
3349/// Description:
3350/// Returns the path and post-transform matrix of the indicated edge.
3351/// If the edge is invalid for the geometry, returns `undef`.
3352function _attach_geom_edge_path(geom, edge) =
3353 assert(is_vector(edge),str("Invalid edge: edge=",edge))
3354 let(
3355 type = geom[0],
3356 cp = _get_cp(geom),
3357 offset_raw = select(geom,-2),
3358 offset = [for (i=[0:2]) edge[i]==0? 0 : offset_raw[i]], // prevents bad centering.
3359 edge = point3d(edge)
3360 )
3361 type == "prismoid"? ( //size, size2, shift, axis
3362 let(all_comps_good = [for (c=edge) if (c!=sign(c)) 1]==[])
3363 assert(all_comps_good, "All components of an edge for a cuboid/prismoid must be -1, 0, or 1")
3364 let(edge_good = len([for (c=edge) if(c) 1])==2)
3365 assert(edge_good, "Invalid edge.")
3366 let(
3367 size = geom[1],
3368 size2 = geom[2],
3369 shift = point2d(geom[3]),
3370 axis = point3d(geom[4]),
3371 edge = rot(from=axis, to=UP, p=edge),
3372 offset = rot(from=axis, to=UP, p=offset),
3373 h = size.z,
3374 cpos = function(vec) let(
3375 u = (vec.z + 1) / 2,
3376 siz = lerp(point2d(size), size2, u) / 2,
3377 z = vec.z * h / 2,
3378 pos = point3d(v_mul(siz, point2d(vec)) + shift * u, z)
3379 ) pos,
3380 ep1 = cpos([for (c=edge) c? c : -1]),
3381 ep2 = cpos([for (c=edge) c? c : 1]),
3382 cp = (ep1 + ep2) / 2,
3383 axy = point2d(edge),
3384 bot = point3d(v_mul(point2d(size )/2, axy), -h/2),
3385 top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2),
3386 xang = atan2(h,(top-bot).x),
3387 yang = atan2(h,(top-bot).y),
3388 vecs = [
3389 if (edge.x) yrot(90-xang, p=sign(axy.x)*RIGHT),
3390 if (edge.y) xrot(yang-90, p=sign(axy.y)*BACK),
3391 if (edge.z) [0,0,sign(edge.z)]
3392 ],
3393 segvec = cross(unit(vecs[1]), unit(vecs[0])),
3394 seglen = norm(ep2 - ep1),
3395 path = [
3396 cp - segvec * seglen/2,
3397 cp + segvec * seglen/2
3398 ],
3399 m = rot(from=UP,to=axis) * move(offset)
3400 ) [path, [vecs], m]
3401 ) : type == "conoid"? ( //r1, r2, l, shift, axis
3402 assert(edge.z && edge.z == sign(edge.z), "The Z component of an edge for a cylinder/cone must be -1 or 1")
3403 let(
3404 rr1 = geom[1],
3405 rr2 = geom[2],
3406 l = geom[3],
3407 shift = point2d(geom[4]),
3408 axis = point3d(geom[5]),
3409 r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1),
3410 r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2),
3411 edge = rot(from=axis, to=UP, p=edge),
3412 offset = rot(from=axis, to=UP, p=offset),
3413 maxr = max([each r1, each r2]),
3414 sides = segs(maxr),
3415 top = path3d(move(shift, p=ellipse(r=r2, $fn=sides)), l/2),
3416 bot = path3d(ellipse(r=r1, $fn=sides), -l/2),
3417 path = edge.z < 0 ? bot : top,
3418 path2 = edge.z < 0 ? top : bot,
3419 zed = edge.z<0? [0,0,-l/2] : point3d(shift,l/2),
3420 vecs = [
3421 for (i = idx(top)) let(
3422 pt1 = (path[i] + select(path,i+1)) /2,
3423 pt2 = (path2[i] + select(path2,i+1)) /2,
3424 v1 = unit(zed - pt1),
3425 v2 = unit(pt2 - pt1),
3426 v3 = unit(cross(v1,v2)),
3427 v4 = cross(v3,v2),
3428 v5 = cross(v1,v3)
3429 ) [v4, v5]
3430 ],
3431 m = rot(from=UP,to=axis) * move(offset)
3432 ) edge.z>0
3433 ? [reverse(list_wrap(path)), reverse(vecs), m]
3434 : [list_wrap(path), vecs, m]
3435 ) : undef;
3436
3437
3438/// Internal Function: _attach_transform()
3439/// Usage: To Get a Transformation Matrix
3440/// mat = _attach_transform(anchor, spin, orient, geom);
3441/// Usage: To Transform Points, Paths, Patches, or VNFs
3442/// new_p = _attach_transform(anchor, spin, orient, geom, p);
3443/// Topics: Attachments
3444/// See Also: reorient(), attachable()
3445/// Description:
3446/// Returns the affine3d transformation matrix needed to `anchor`, `spin`, and `orient`
3447/// the given geometry `geom` shape into position.
3448/// Arguments:
3449/// anchor = Anchor point to translate to the origin `[0,0,0]`. See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3450/// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3451/// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3452/// geom = The geometry description of the shape.
3453/// p = If given as a VNF, path, or point, applies the affine3d transformation matrix to it and returns the result.
3454
3455function _attach_transform(anchor, spin, orient, geom, p) =
3456 assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Got: ",anchor))
3457 assert(is_undef(spin) || is_vector(spin,3) || is_num(spin), str("Got: ",spin))
3458 assert(is_undef(orient) || is_vector(orient,3), str("Got: ",orient))
3459 let(
3460 anchor = default(anchor, CENTER),
3461 spin = default(spin, 0),
3462 orient = default(orient, UP),
3463 two_d = _attach_geom_2d(geom),
3464 m = ($attach_to != undef)? (
3465 let(
3466 anch = _find_anchor($attach_to, geom),
3467 pos = is_undef($anchor_override) ? anch[1]
3468 : _find_anchor(_make_anchor_legal($anchor_override,geom),geom)[1]
3469 )
3470 two_d?
3471 assert(is_num(spin))
3472 /*affine3d_zrot(spin) * */
3473 rot(to=FWD, from=point3d(anch[2]))
3474 * affine3d_translate(point3d(-pos))
3475 :
3476 assert(is_num(spin) || is_vector(spin,3))
3477 let(
3478 ang = vector_angle(anch[2], DOWN),
3479 axis = vector_axis(anch[2], DOWN),
3480 ang2 = (anch[2]==UP || anch[2]==DOWN)? 0 : 180-anch[3],
3481 axis2 = rot(p=axis,[0,0,ang2])
3482 )
3483 affine3d_rot_by_axis(axis2,ang)
3484 * (is_num(spin)? affine3d_zrot(ang2+spin)
3485 : affine3d_zrot(spin.z) * affine3d_yrot(spin.y) * affine3d_xrot(spin.x)
3486 * affine3d_zrot(ang2))
3487 * affine3d_translate(point3d(-pos))
3488 ) : (
3489 let(
3490 anchor = is_undef($attach_alignment) ? anchor
3491 : two_d? _make_anchor_legal(zrot(-spin,$attach_alignment),geom)
3492 : _make_anchor_legal(rot(spin, from=UP,to=orient,reverse=true,p=$attach_alignment),geom),
3493 pos = _find_anchor(anchor, geom)[1]
3494 )
3495 two_d?
3496 assert(is_num(spin))
3497 affine3d_zrot(spin) * affine3d_translate(point3d(-pos))
3498 :
3499 assert(is_num(spin) || is_vector(spin,3))
3500 let(
3501 axis = vector_axis(UP,orient),
3502 ang = vector_angle(UP,orient)
3503 )
3504 affine3d_rot_by_axis(axis,ang)
3505 * ( is_num(spin)? affine3d_zrot(spin)
3506 : affine3d_zrot(spin.z) * affine3d_yrot(spin.y) * affine3d_xrot(spin.x))
3507 * affine3d_translate(point3d(-pos))
3508 )
3509 )
3510 is_undef(p)? m
3511 : is_vnf(p) && p==EMPTY_VNF? p
3512 : apply(m, p);
3513
3514
3515function _get_cp(geom) =
3516 let(cp=select(geom,-3))
3517 is_vector(cp) ? cp
3518 : let(
3519 type = in_list(geom[0],["vnf_extent","vnf_isect"]) ? "vnf"
3520 : in_list(geom[0],["rgn_extent","rgn_isect"]) ? "path"
3521 : in_list(geom[0],["extrusion_extent","extrusion_isect"]) ? "xpath"
3522 : "other"
3523 )
3524 assert(type!="other", "Invalid cp value")
3525 cp=="centroid" ? (
3526 type=="vnf" && (len(geom[1][0])==0 || len(geom[1][1])==0) ? [0,0,0] :
3527 [each centroid(geom[1]), if (type=="xpath") 0]
3528 )
3529 : let(points = type=="vnf"?geom[1][0]:flatten(force_region(geom[1])))
3530 cp=="mean" ? [each mean(points), if (type=="xpath") 0]
3531 : cp=="box" ?[each mean(pointlist_bounds(points)), if (type=="xpath") 0]
3532 : assert(false,"Invalid cp specification");
3533
3534
3535function _get_cp(geom) =
3536 let(cp=select(geom,-3))
3537 is_vector(cp) ? cp
3538 : let(
3539 is_vnf = in_list(geom[0],["vnf_extent","vnf_isect"])
3540 )
3541 cp == "centroid" ? (
3542 is_vnf && len(geom[1][1])==0
3543 ? [0,0,0]
3544 : centroid(geom[1])
3545 )
3546 : let(points = is_vnf?geom[1][0]:flatten(force_region(geom[1])))
3547 cp=="mean" ? mean(points)
3548 : cp=="box" ? mean(pointlist_bounds(points))
3549 : assert(false,"Invalid cp specification");
3550
3551
3552
3553function _force_anchor_2d(anchor) =
3554 is_undef(anchor) || len(anchor)==2 ? anchor :
3555 assert(anchor.y==0 || anchor.z==0, "Anchor for a 2D shape cannot be fully 3D. It must have either Y or Z component equal to zero.")
3556 anchor.y==0 ? [anchor.x,anchor.z] : point2d(anchor);
3557
3558
3559/// Internal Function: _find_anchor()
3560/// Usage:
3561/// anchorinfo = _find_anchor(anchor, geom);
3562/// Topics: Attachments
3563/// See Also: reorient(), attachable()
3564/// Description:
3565/// Calculates the anchor data for the given `anchor` vector or name, in the given attachment
3566/// geometry. Returns `[ANCHOR, POS, VEC, ANG]` where `ANCHOR` is the requested anchorname
3567/// or vector, `POS` is the anchor position, `VEC` is the direction vector of the anchor, and
3568/// `ANG` is the angle to align with around the rotation axis of th anchor direction vector.
3569/// Arguments:
3570/// anchor = Vector or named anchor string.
3571/// geom = The geometry description of the shape.
3572function _find_anchor(anchor, geom) =
3573 is_string(anchor)? (
3574 anchor=="origin"? [anchor, CENTER, UP, 0] // Ok that this returns 3d anchor in the 2d case?
3575 : let(
3576 anchors = last(geom),
3577 found = search([anchor], anchors, num_returns_per_match=1)[0]
3578 )
3579 assert(found!=[], str("Unknown anchor: ",anchor))
3580 anchors[found]
3581 ) :
3582 let(
3583 cp = _get_cp(geom),
3584 offset_raw = select(geom,-2),
3585 offset = [for (i=[0:2]) anchor[i]==0? 0 : offset_raw[i]], // prevents bad centering.
3586 type = geom[0]
3587 )
3588 assert(is_vector(anchor),str("Invalid anchor: anchor=",anchor))
3589 let(
3590 anchor = point3d(anchor),
3591 oang = (
3592 approx(point2d(anchor), [0,0])? 0 :
3593 atan2(anchor.y, anchor.x)+90
3594 )
3595 )
3596 type == "prismoid"? ( //size, size2, shift, axis
3597 let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[])
3598 assert(all_comps_good, "All components of an anchor for a cuboid/prismoid must be -1, 0, or 1")
3599 let(
3600 size=geom[1], size2=geom[2],
3601 shift=point2d(geom[3]), axis=point3d(geom[4]),
3602 override = geom[5](anchor)
3603 )
3604 let(
3605 size = [for (c = size) max(0,c)],
3606 size2 = [for (c = size2) max(0,c)],
3607 anch = rot(from=axis, to=UP, p=anchor),
3608 offset = rot(from=axis, to=UP, p=offset),
3609 h = size.z,
3610 u = (anch.z + 1) / 2, // u is one of 0, 0.5, or 1
3611 axy = point2d(anch),
3612 bot = point3d(v_mul(point2d(size )/2, axy), -h/2),
3613 top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2),
3614 pos = point3d(cp) + lerp(bot,top,u) + offset,
3615 vecs = anchor==CENTER? [UP]
3616 : [
3617 if (anch.x!=0) unit(rot(from=UP, to=[(top-bot).x,0,max(0.01,h)], p=[axy.x,0,0]), UP),
3618 if (anch.y!=0) unit(rot(from=UP, to=[0,(top-bot).y,max(0.01,h)], p=[0,axy.y,0]), UP),
3619 if (anch.z!=0) unit([0,0,anch.z],UP)
3620 ],
3621 vec2 = anchor==CENTER? UP
3622 : len(vecs)==1? unit(vecs[0],UP)
3623 : len(vecs)==2? vector_bisect(vecs[0],vecs[1])
3624 : let(
3625 v1 = vector_bisect(vecs[0],vecs[2]),
3626 v2 = vector_bisect(vecs[1],vecs[2]),
3627 p1 = plane_from_normal(yrot(90,p=v1)),
3628 p2 = plane_from_normal(xrot(-90,p=v2)),
3629 line = plane_intersection(p1,p2),
3630 v3 = unit(line[1]-line[0],UP) * anch.z
3631 )
3632 unit(v3,UP),
3633 vec = default(override[1],rot(from=UP, to=axis, p=vec2)),
3634 pos2 = default(override[0],rot(from=UP, to=axis, p=pos))
3635 ) [anchor, pos2, vec, default(override[2],oang)]
3636 ) : type == "conoid"? ( //r1, r2, l, shift
3637 assert(anchor.z == sign(anchor.z), "The Z component of an anchor for a cylinder/cone must be -1, 0, or 1")
3638 let(
3639 rr1=geom[1], rr2=geom[2], l=geom[3],
3640 shift=point2d(geom[4]), axis=point3d(geom[5]),
3641 r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1),
3642 r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2),
3643 anch = rot(from=axis, to=UP, p=anchor),
3644 offset = rot(from=axis, to=UP, p=offset),
3645 u = (anch.z+1)/2,
3646 axy = unit(point2d(anch),[0,0]),
3647 bot = point3d(v_mul(r1,axy), -l/2),
3648 top = point3d(v_mul(r2,axy)+shift, l/2),
3649 pos = point3d(cp) + lerp(bot,top,u) + offset,
3650 sidevec = rot(from=UP, to=top==bot?UP:top-bot, p=point3d(axy)),
3651 vvec = anch==CENTER? UP : unit([0,0,anch.z],UP),
3652 vec = anch==CENTER? CENTER :
3653 approx(axy,[0,0])? unit(anch,UP) :
3654 approx(anch.z,0)? sidevec :
3655 unit((sidevec+vvec)/2,UP),
3656 pos2 = rot(from=UP, to=axis, p=pos),
3657 vec2 = anch==CENTER? UP : rot(from=UP, to=axis, p=vec)
3658 ) [anchor, pos2, vec2, oang]
3659 ) : type == "point"? (
3660 let(
3661 anchor = unit(point3d(anchor),CENTER),
3662 pos = point3d(cp) + point3d(offset),
3663 vec = unit(anchor,UP)
3664 ) [anchor, pos, vec, oang]
3665 ) : type == "spheroid"? ( //r
3666 let(
3667 rr = geom[1],
3668 r = is_num(rr)? [rr,rr,rr] : point3d(rr),
3669 anchor = unit(point3d(anchor),CENTER),
3670 pos = point3d(cp) + v_mul(r,anchor) + point3d(offset),
3671 vec = unit(v_mul(r,anchor),UP)
3672 ) [anchor, pos, vec, oang]
3673 ) : type == "vnf_isect"? ( //vnf
3674 let( vnf=geom[1] )
3675 approx(anchor,CTR)? [anchor, cp, UP, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3676 vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor), 0] :
3677 let(
3678 eps = 1/2048,
3679 points = vnf[0],
3680 faces = vnf[1],
3681 rpts = apply(rot(from=anchor, to=RIGHT) * move(-cp), points),
3682 hits = [
3683 for (face = faces)
3684 let(
3685 verts = select(rpts, face),
3686 ys = column(verts,1),
3687 zs = column(verts,2)
3688 )
3689 if (max(ys) >= -eps && max(zs) >= -eps &&
3690 min(ys) <= eps && min(zs) <= eps)
3691 let(
3692 poly = select(points, face),
3693 isect = polygon_line_intersection(poly, [cp,cp+anchor], eps=eps),
3694 ptlist = is_undef(isect) ? [] :
3695 is_vector(isect) ? [isect]
3696 : flatten(isect), // parallel to a face
3697 n = len(ptlist)>0 ? polygon_normal(poly) : undef
3698 )
3699 for(pt=ptlist) [anchor * (pt-cp), n, pt]
3700 ]
3701 )
3702 assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.")
3703 let(
3704 furthest = max_index(column(hits,0)),
3705 dist = hits[furthest][0],
3706 pos = hits[furthest][2],
3707 hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]],
3708 unorms = [
3709 for (i = idx(hitnorms))
3710 let(
3711 thisnorm = hitnorms[i],
3712 isdup = [
3713 for (j = [i+1:1:len(hitnorms)-1])
3714 if (approx(thisnorm, hitnorms[j])) 1
3715 ] != []
3716 )
3717 if (!isdup) thisnorm
3718 ],
3719 n = unit(sum(unorms)),
3720 oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90
3721 )
3722 [anchor, pos, n, oang]
3723 ) : type == "vnf_extent"? ( //vnf
3724 let( vnf=geom[1] )
3725 approx(anchor,CTR)? [anchor, cp, UP, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3726 vnf==EMPTY_VNF? [anchor, [0,0,0], unit(anchor,UP), 0] :
3727 let(
3728 rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]),
3729 maxx = max(column(rpts,0)),
3730 idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i],
3731 avep = sum(select(rpts,idxs))/len(idxs),
3732 mpt = approx(point2d(anchor),[0,0])? [maxx,0,0] : avep,
3733 pos = point3d(cp) + rot(from=RIGHT, to=anchor, p=mpt)
3734 ) [anchor, pos, anchor, oang]
3735 ) : type == "trapezoid"? ( //size, size2, shift, override
3736 let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[])
3737 assert(all_comps_good, "All components of an anchor for a rectangle/trapezoid must be -1, 0, or 1")
3738 let(
3739 anchor=_force_anchor_2d(anchor),
3740 size=geom[1], size2=geom[2], shift=geom[3],
3741 u = (anchor.y+1)/2, // 0<=u<=1
3742 frpt = [size.x/2*anchor.x, -size.y/2],
3743 bkpt = [size2/2*anchor.x+shift, size.y/2],
3744 override = geom[4](anchor),
3745 pos = override[0] != undef? override[0] :
3746 point2d(cp) + lerp(frpt, bkpt, u) + point2d(offset),
3747 svec = approx(bkpt,frpt)? [anchor.x,0,0] :
3748 point3d(line_normal(bkpt,frpt)*anchor.x),
3749 vec = is_def(override[1]) ? override[1]
3750 : anchor.y == 0? ( anchor.x == 0? BACK : svec )
3751 : anchor.x == 0? [0,anchor.y,0]
3752 : unit((svec + [0,anchor.y,0]) / 2, [0,anchor.y,0])
3753 ) [anchor, pos, vec, 0]
3754 ) : type == "ellipse"? ( //r
3755 let(
3756 anchor = unit(_force_anchor_2d(anchor),[0,0]),
3757 r = force_list(geom[1],2),
3758 pos = approx(anchor.x,0)
3759 ? [0,sign(anchor.y)*r.y]
3760 : let(
3761 m = anchor.y/anchor.x,
3762 px = approx(min(r),0)? 0 :
3763 sign(anchor.x) * sqrt(1/(1/sqr(r.x) + m*m/sqr(r.y)))
3764 )
3765 [px,m*px],
3766 vec = approx(min(r),0)? (approx(norm(anchor),0)? BACK : anchor) :
3767 unit([r.y/r.x*pos.x, r.x/r.y*pos.y],BACK)
3768 ) [anchor, point2d(cp+offset)+pos, vec, 0]
3769 ) : type == "rgn_isect"? ( //region
3770 let(
3771 anchor = _force_anchor_2d(anchor),
3772 rgn = force_region(move(-point2d(cp), p=geom[1]))
3773 )
3774 approx(anchor,[0,0])? [anchor, cp, BACK, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3775 let(
3776 isects = [
3777 for (path=rgn, t=triplet(path,true)) let(
3778 seg1 = [t[0],t[1]],
3779 seg2 = [t[1],t[2]],
3780 isect = line_intersection([[0,0],anchor], seg1, RAY, SEGMENT),
3781 n = is_undef(isect)? [0,1] :
3782 !approx(isect, t[1])? line_normal(seg1) :
3783 unit((line_normal(seg1)+line_normal(seg2))/2,[0,1]),
3784 n2 = vector_angle(anchor,n)>90? -n : n
3785 )
3786 if(!is_undef(isect) && !approx(isect,t[0])) [norm(isect), isect, n2]
3787 ]
3788 )
3789 assert(len(isects)>0, "Anchor vector does not intersect with the shape. Attachment failed.")
3790 let(
3791 maxidx = max_index(column(isects,0)),
3792 isect = isects[maxidx],
3793 pos = point2d(cp) + isect[1],
3794 vec = unit(isect[2],[0,1])
3795 ) [anchor, pos, vec, 0]
3796 ) : type == "rgn_extent"? ( //region
3797 let( anchor = _force_anchor_2d(anchor) )
3798 approx(anchor,[0,0])? [anchor, cp, BACK, 0] : // CENTER anchors anchor on cp, "origin" anchors on [0,0]
3799 let(
3800 rgn = force_region(geom[1]),
3801 rpts = rot(from=anchor, to=RIGHT, p=flatten(rgn)),
3802 maxx = max(column(rpts,0)),
3803 ys = [for (pt=rpts) if (approx(pt.x, maxx)) pt.y],
3804 midy = (min(ys)+max(ys))/2,
3805 pos = rot(from=RIGHT, to=anchor, p=[maxx,midy])
3806 ) [anchor, pos, unit(anchor,BACK), 0]
3807 ) : type=="extrusion_extent" || type=="extrusion_isect" ? ( // extruded region
3808 assert(in_list(anchor.z,[-1,0,1]), "The Z component of an anchor for an extruded 2D shape must be -1, 0, or 1.")
3809 let(
3810 anchor_xy = point2d(anchor),
3811 rgn = geom[1],
3812 L = geom[2],
3813 twist = geom[3],
3814 scale = geom[4],
3815 shift = geom[5],
3816 u = (anchor.z + 1) / 2,
3817 shmat = move(lerp([0,0], shift, u)),
3818 scmat = scale(lerp([1,1], scale, u)),
3819 twmat = zrot(lerp(0, -twist, u)),
3820 mat = shmat * scmat * twmat
3821 )
3822 approx(anchor_xy,[0,0]) ? [anchor, apply(mat, point3d(cp,anchor.z*L/2)), unit(anchor, UP), oang] :
3823 let(
3824 newrgn = apply(mat, rgn),
3825 newgeom = attach_geom(two_d=true, region=newrgn, extent=type=="extrusion_extent", cp=cp),
3826 topmat = anchor.z!=0 ? []
3827 : move(shift)*scale(scale)*zrot(-twist),
3828 topgeom = anchor.z!=0? []
3829 : attach_geom(two_d=true, region=apply(topmat,rgn), extent=type=="extrusion_extent", cp=cp),
3830 top2d = anchor.z!=0? []
3831 : _find_anchor(anchor_xy, topgeom),
3832 result2d = _find_anchor(anchor_xy, newgeom),
3833 pos = point3d(result2d[1], anchor.z*L/2),
3834 vec = anchor.z==0? rot(from=UP,to=point3d(top2d[1],L/2)-point3d(result2d[1]),p=point3d(result2d[2]))
3835 : unit(point3d(result2d[2], anchor.z),UP),
3836 oang = atan2(vec.y,vec.x) + 90
3837 )
3838 [anchor, pos, vec, oang]
3839 ) :
3840 assert(false, "Unknown attachment geometry type.");
3841
3842
3843/// Internal Function: _is_shown()
3844/// Usage:
3845/// bool = _is_shown();
3846/// Topics: Attachments
3847/// See Also: reorient(), attachable()
3848/// Description:
3849/// Returns true if objects should currently be shown based on the tag settings.
3850function _is_shown() =
3851 assert(is_list($tags_shown) || $tags_shown=="ALL")
3852 assert(is_list($tags_hidden))
3853 let(
3854 dummy=is_undef($tags) ? 0 : echo("Use tag() instead of $tags for specifying an object's tag."),
3855 $tag = default($tag,$tags)
3856 )
3857 assert(is_string($tag), str("Tag value (",$tag,") is not a string"))
3858 assert(undef==str_find($tag," "),str("Tag string \"",$tag,"\" contains a space, which is not allowed"))
3859 let(
3860 shown = $tags_shown=="ALL" || in_list($tag,$tags_shown),
3861 hidden = in_list($tag, $tags_hidden)
3862 )
3863 shown && !hidden;
3864
3865
3866// Section: Visualizing Anchors
3867
3868/// Internal Function: _standard_anchors()
3869/// Usage:
3870/// anchs = _standard_anchors([two_d]);
3871/// Description:
3872/// Return the vectors for all standard anchors.
3873/// Arguments:
3874/// two_d = If true, returns only the anchors where the Z component is 0. Default: false
3875function _standard_anchors(two_d=false) = [
3876 for (
3877 zv = [
3878 if (!two_d) TOP,
3879 CENTER,
3880 if (!two_d) BOTTOM
3881 ],
3882 yv = [FRONT, CENTER, BACK],
3883 xv = [LEFT, CENTER, RIGHT]
3884 ) xv+yv+zv
3885];
3886
3887
3888
3889// Module: show_anchors()
3890// Synopsis: Shows anchors for the parent object.
3891// SynTags: Geom
3892// Topics: Attachments
3893// See Also: expose_anchors(), anchor_arrow(), anchor_arrow2d(), frame_ref()
3894// Usage:
3895// PARENT() show_anchors([s], [std=], [custom=]);
3896// Description:
3897// Show all standard anchors for the parent object.
3898// Arguments:
3899// s = Length of anchor arrows.
3900// ---
3901// std = If true show standard anchors. Default: true
3902// custom = If true show named anchors. Default: true
3903// Example(FlatSpin,VPD=333):
3904// cube(50, center=true) show_anchors();
3905module show_anchors(s=10, std=true, custom=true) {
3906 check = assert($parent_geom != undef);
3907 two_d = _attach_geom_2d($parent_geom);
3908 if (std) {
3909 for (anchor=_standard_anchors(two_d=two_d)) {
3910 if(two_d) {
3911 attach(anchor) anchor_arrow2d(s);
3912 } else {
3913 attach(anchor) anchor_arrow(s);
3914 }
3915 }
3916 }
3917 if (custom) {
3918 for (anchor=last($parent_geom)) {
3919 attach(anchor[0]) {
3920 if(two_d) {
3921 anchor_arrow2d(s, color="cyan");
3922 } else {
3923 anchor_arrow(s, color="cyan");
3924 }
3925 color("black")
3926 tag("anchor-arrow") {
3927 xrot(two_d? 0 : 90) {
3928 back(s/3) {
3929 yrot_copies(n=2)
3930 up(two_d? 0.51 : s/30) {
3931 linear_extrude(height=0.01, convexity=12, center=true) {
3932 text(text=anchor[0], size=s/4, halign="center", valign="center", font="Helvetica", $fn=36);
3933 }
3934 }
3935 }
3936 }
3937 }
3938 color([1, 1, 1, 1])
3939 tag("anchor-arrow") {
3940 xrot(two_d? 0 : 90) {
3941 back(s/3) {
3942 cube([s/4.5*len(anchor[0]), s/3, 0.01], center=true);
3943 }
3944 }
3945 }
3946 }
3947 }
3948 }
3949 children();
3950}
3951
3952
3953// Module: anchor_arrow()
3954// Synopsis: Shows a 3d anchor orientation arrow.
3955// SynTags: Geom
3956// Topics: Attachments
3957// See Also: anchor_arrow2d(), show_anchors(), expose_anchors(), frame_ref(), generic_airplane()
3958// Usage:
3959// anchor_arrow([s], [color], [flag], [anchor=], [orient=], [spin=]) [ATTACHMENTS];
3960// Description:
3961// Show an anchor orientation arrow. By default, tagged with the name "anchor-arrow".
3962// Arguments:
3963// s = Length of the arrows. Default: `10`
3964// color = Color of the arrow. Default: `[0.333, 0.333, 1]`
3965// flag = If true, draw the orientation flag on the arrowhead. Default: true
3966// ---
3967// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
3968// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
3969// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
3970// Example:
3971// anchor_arrow(s=20);
3972module anchor_arrow(s=10, color=[0.333,0.333,1], flag=true, $tag="anchor-arrow", $fn=12, anchor=BOT, spin=0, orient=UP) {
3973 attachable(anchor,spin,orient, r=s/6, l=s) {
3974 down(s/2)
3975 recolor("gray") spheroid(d=s/6) {
3976 attach(CENTER,BOT) recolor(color) cyl(h=s*2/3, d=s/15) {
3977 attach(TOP,BOT) cyl(h=s/3, d1=s/5, d2=0) {
3978 if(flag) {
3979 position(BOT)
3980 recolor([1,0.5,0.5])
3981 cuboid([s/100, s/6, s/4], anchor=FRONT+BOT);
3982 }
3983 }
3984 }
3985 }
3986 children();
3987 }
3988}
3989
3990
3991
3992// Module: anchor_arrow2d()
3993// Synopsis: Shows a 2d anchor orientation arrow.
3994// SynTags: Geom
3995// Topics: Attachments
3996// See Also: anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
3997// Usage:
3998// anchor_arrow2d([s], [color]);
3999// Description:
4000// Show an anchor orientation arrow.
4001// Arguments:
4002// s = Length of the arrows.
4003// color = Color of the arrow.
4004// Example:
4005// anchor_arrow2d(s=20);
4006module anchor_arrow2d(s=15, color=[0.333,0.333,1], $tag="anchor-arrow") {
4007 color(color) stroke([[0,0],[0,s]], width=s/10, endcap1="butt", endcap2="arrow2");
4008}
4009
4010
4011
4012// Module: expose_anchors()
4013// Synopsis: Used to show a transparent object with solid color anchor arrows.
4014// Topics: Attachments
4015// See Also: anchor_arrow2d(), show_anchors(), show_anchors(), frame_ref()
4016// Usage:
4017// expose_anchors(opacity) {child1() show_anchors(); child2() show_anchors(); ...}
4018// Description:
4019// Used in combination with show_anchors() to display an object in transparent gray with its anchors in solid color.
4020// Children will appear transparent and any anchor arrows drawn with will appear in solid color.
4021// Arguments:
4022// opacity = The opacity of the children. 0.0 is invisible, 1.0 is opaque. Default: 0.2
4023// Example(FlatSpin,VPD=333):
4024// expose_anchors() cube(50, center=true) show_anchors();
4025module expose_anchors(opacity=0.2) {
4026 show_only("anchor-arrow")
4027 children();
4028 hide("anchor-arrow")
4029 color(is_undef($color) || $color=="default" ? [0,0,0] :
4030 is_string($color) ? $color
4031 : point3d($color),
4032 opacity)
4033 children();
4034}
4035
4036
4037
4038// Module: show_transform_list()
4039// Synopsis: Shows a list of transforms and how they connect.
4040// SynTags: Geom
4041// Topics: Attachments
4042// See Also: generic_airplane(), anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
4043// Usage:
4044// show_transform_list(tlist, [s]);
4045// show_transform_list(tlist) {CHILDREN};
4046// Description:
4047// Given a list of transformation matrices, shows the position and orientation of each one.
4048// A line is drawn from each transform position to the next one, and an orientation indicator is
4049// shown at each position. If a child is passed, that child will be used as the orientation indicator.
4050// By default, a {{generic_airplane()}} is used as the orientation indicator.
4051// Arguments:
4052// s = Length of the {{generic_airplane()}}. Default: 5
4053// Example:
4054// tlist = [
4055// zrot(90),
4056// zrot(90) * fwd(30) * zrot(30),
4057// zrot(90) * fwd(30) * zrot(30) *
4058// fwd(35) * xrot(-30),
4059// zrot(90) * fwd(30) * zrot(30) *
4060// fwd(35) * xrot(-30) * fwd(40) * yrot(15),
4061// ];
4062// show_transform_list(tlist, s=20);
4063// Example:
4064// tlist = [
4065// zrot(90),
4066// zrot(90) * fwd(30) * zrot(30),
4067// zrot(90) * fwd(30) * zrot(30) *
4068// fwd(35) * xrot(-30),
4069// zrot(90) * fwd(30) * zrot(30) *
4070// fwd(35) * xrot(-30) * fwd(40) * yrot(15),
4071// ];
4072// show_transform_list(tlist) frame_ref();
4073module show_transform_list(tlist, s=5) {
4074 path = [for (m = tlist) apply(m, [0,0,0])];
4075 stroke(path, width=s*0.03);
4076 for (m = tlist) {
4077 multmatrix(m) {
4078 if ($children>0) children();
4079 else generic_airplane(s=s);
4080 }
4081 }
4082}
4083
4084
4085// Module: generic_airplane()
4086// Synopsis: Shows a generic airplane shape, useful for viewing orientations.
4087// SynTags: Geom
4088// Topics: Attachments
4089// See Also: anchor_arrow(), show_anchors(), expose_anchors(), frame_ref()
4090// Usage:
4091// generic_airplane([s]);
4092// Description:
4093// Creates a generic airplane shape. This can be useful for viewing the orientation of 3D transforms.
4094// Arguments:
4095// s = Length of the airplane. Default: 5
4096// Example:
4097// generic_airplane(s=20);
4098module generic_airplane(s=5) {
4099 $fn = max(segs(0.05*s), 12);
4100 color("#ddd")
4101 fwd(s*0.05)
4102 ycyl(l=0.7*s, d=0.1*s) {
4103 attach(FWD) top_half(s=s) zscale(2) sphere(d=0.1*s);
4104 attach(BACK,FWD) ycyl(l=0.2*s, d1=0.1*s, d2=0.05*s) {
4105 yrot_copies([-90,0,90])
4106 prismoid(s*[0.01,0.2], s*[0.01,0.05],
4107 h=0.2*s, shift=s*[0,0.15], anchor=BOT);
4108 }
4109 yrot_copies([-90,90])
4110 prismoid(s*[0.01,0.2], s*[0.01,0.05],
4111 h=0.5*s, shift=s*[0,0.15], anchor=BOT);
4112 }
4113 color("#777") zcopies(0.1*s) sphere(d=0.02*s);
4114 back(0.09*s) {
4115 color("#f00") right(0.46*s) sphere(d=0.04*s);
4116 color("#0f0") left(0.46*s) sphere(d=0.04*s);
4117 }
4118}
4119
4120
4121
4122// Module: frame_ref()
4123// Synopsis: Shows axis orientation arrows.
4124// SynTags: Geom
4125// Topics: Attachments
4126// See Also: anchor_arrow(), anchor_arrow2d(), show_anchors(), expose_anchors()
4127// Usage:
4128// frame_ref(s, opacity);
4129// Description:
4130// Displays X,Y,Z axis arrows in red, green, and blue respectively.
4131// Arguments:
4132// s = Length of the arrows.
4133// opacity = The opacity of the arrows. 0.0 is invisible, 1.0 is opaque. Default: 1.0
4134// Examples:
4135// frame_ref(25);
4136// frame_ref(30, opacity=0.5);
4137module frame_ref(s=15, opacity=1) {
4138 cube(0.01, center=true) {
4139 attach([1,0,0]) anchor_arrow(s=s, flag=false, color=[1.0, 0.3, 0.3, opacity]);
4140 attach([0,1,0]) anchor_arrow(s=s, flag=false, color=[0.3, 1.0, 0.3, opacity]);
4141 attach([0,0,1]) anchor_arrow(s=s, flag=false, color=[0.3, 0.3, 1.0, opacity]);
4142 children();
4143 }
4144}
4145
4146
4147////////////////////////////////////////////////////////////////////////////////////////////////////
4148////////////////////////////////////////////////////////////////////////////////////////////////////
4149////////////////////////////////////////////////////////////////////////////////////////////////////
4150////////////////////////////////////////////////////////////////////////////////////////////////////
4151///
4152/// Code after this is internal code for managing edge and corner sets and for displaying
4153/// edge and corners in the docs
4154///
4155
4156module _edges_text3d(txt,size=3) {
4157 if (is_list(txt)) {
4158 for (i=idx(txt)) {
4159 down((i-len(txt)/2+0.5)*size*1.5) {
4160 _edges_text3d(txt[i], size=size);
4161 }
4162 }
4163 } else {
4164 xrot(90) color("#000")
4165 linear_extrude(height=0.1) {
4166 text(text=txt, size=size, halign="center", valign="center");
4167 }
4168 }
4169}
4170
4171
4172function _edges_vec_txt(x) = is_string(x)? str("\"", x, "\"") :
4173 assert(is_string(x) || is_vector(x,3), str(x))
4174 let(
4175 lst = concat(
4176 x.z>0? ["TOP"] : x.z<0? ["BOT"] : [],
4177 x.y>0? ["BACK"] : x.y<0? ["FWD"] : [],
4178 x.x>0? ["RIGHT"] : x.x<0? ["LEFT"] : []
4179 ),
4180 out = [
4181 for (i = idx(lst))
4182 i>0? str("+",lst[i]) : lst[i]
4183 ]
4184 ) out;
4185
4186
4187function _edges_text(edges) =
4188 is_string(edges) ? [str("\"",edges,"\"")] :
4189 edges==EDGES_NONE ? ["EDGES_NONE"] :
4190 edges==EDGES_ALL ? ["EDGES_ALL"] :
4191 _is_edge_array(edges) ? [""] :
4192 is_vector(edges,3) ? _edges_vec_txt(edges) :
4193 is_list(edges) ? let(
4194 lst = [for (x=edges) each _edges_text(x)],
4195 out = [
4196 for (i=idx(lst))
4197 str(
4198 (i==0? "[" : ""),
4199 lst[i],
4200 (i<len(lst)-1? "," : ""),
4201 (i==len(lst)-1? "]" : "")
4202 )
4203 ]
4204 ) out :
4205 [""];
4206
4207
4208
4209/// Internal Constant: EDGES_NONE
4210/// Topics: Edges
4211/// See Also: EDGES_ALL, edges()
4212/// Description:
4213/// The set of no edges.
4214/// Figure(3D):
4215/// _show_edges(edges="NONE");
4216EDGES_NONE = [[0,0,0,0], [0,0,0,0], [0,0,0,0]];
4217
4218
4219/// Internal Constant: EDGES_ALL
4220/// Topics: Edges
4221/// See Also: EDGES_NONE, edges()
4222/// Description:
4223/// The set of all edges.
4224/// Figure(3D):
4225/// _show_edges(edges="ALL");
4226EDGES_ALL = [[1,1,1,1], [1,1,1,1], [1,1,1,1]];
4227
4228
4229/// Internal Constant: EDGES_OFFSETS
4230/// Topics: Edges
4231/// See Also: EDGES_NONE, EDGES_ALL, edges()
4232/// Description:
4233/// The vectors pointing to the center of each edge of a unit sized cube.
4234/// Each item in an edge array will have a corresponding vector in this array.
4235EDGE_OFFSETS = [
4236 [
4237 [ 0,-1,-1],
4238 [ 0, 1,-1],
4239 [ 0,-1, 1],
4240 [ 0, 1, 1]
4241 ], [
4242 [-1, 0,-1],
4243 [ 1, 0,-1],
4244 [-1, 0, 1],
4245 [ 1, 0, 1]
4246 ], [
4247 [-1,-1, 0],
4248 [ 1,-1, 0],
4249 [-1, 1, 0],
4250 [ 1, 1, 0]
4251 ]
4252];
4253
4254
4255
4256/// Internal Function: _is_edge_array()
4257/// Topics: Edges, Type Checking
4258/// Usage:
4259/// bool = _is_edge_array(x);
4260/// Description:
4261/// Returns true if the given value has the form of an edge array.
4262/// Arguments:
4263/// x = The item to check the type of.
4264/// See Also: edges(), EDGES_NONE, EDGES_ALL
4265function _is_edge_array(x) = is_list(x) && is_vector(x[0]) && len(x)==3 && len(x[0])==4;
4266
4267
4268function _edge_set(v) =
4269 _is_edge_array(v)? v : [
4270 for (ax=[0:2]) [
4271 for (b=[-1,1], a=[-1,1]) let(
4272 v2=[[0,a,b],[a,0,b],[a,b,0]][ax]
4273 ) (
4274 is_string(v)? (
4275 v=="X"? (ax==0) : // Return all X axis aligned edges.
4276 v=="Y"? (ax==1) : // Return all Y axis aligned edges.
4277 v=="Z"? (ax==2) : // Return all Z axis aligned edges.
4278 v=="ALL"? true : // Return all edges.
4279 v=="NONE"? false : // Return no edges.
4280 let(valid_values = ["X", "Y", "Z", "ALL", "NONE"])
4281 assert(
4282 in_list(v, valid_values),
4283 str(v, " must be a vector, edge array, or one of ", valid_values)
4284 ) v
4285 ) :
4286 let(nonz = sum(v_abs(v)))
4287 nonz==2? (v==v2) : // Edge: return matching edge.
4288 let(
4289 matches = num_true([
4290 for (i=[0:2]) v[i] && (v[i]==v2[i])
4291 ])
4292 )
4293 nonz==1? (matches==1) : // Face: return surrounding edges.
4294 (matches==2) // Corner: return touching edges.
4295 )? 1 : 0
4296 ]
4297];
4298
4299
4300/// Internal Function: _normalize_edges()
4301/// Topics: Edges
4302/// Usage:
4303/// edges = _normalize_edges(v);
4304/// Description:
4305/// Normalizes all values in an edge array to be `1`, if it was originally greater than `0`,
4306/// or `0`, if it was originally less than or equal to `0`.
4307/// See Also: edges(), EDGES_NONE, EDGES_ALL
4308function _normalize_edges(v) = [for (ax=v) [for (edge=ax) edge>0? 1 : 0]];
4309
4310
4311
4312
4313/// Internal Function: _edges()
4314/// Topics: Edges
4315/// Usage:
4316/// edgs = _edges(v);
4317/// edgs = _edges(v, except);
4318///
4319/// Description:
4320/// Takes a list of edge set descriptors, and returns a normalized edges array
4321/// that represents all those given edges.
4322/// Arguments:
4323/// v = The edge set to include.
4324/// except = The edge set to specifically exclude, even if they are in `v`.
4325///
4326/// See Also: EDGES_NONE, EDGES_ALL
4327///
4328function _edges(v, except=[]) =
4329 v==[] ? EDGES_NONE :
4330 (is_string(v) || is_vector(v) || _is_edge_array(v))? _edges([v], except=except) :
4331 (is_string(except) || is_vector(except) || _is_edge_array(except))? _edges(v, except=[except]) :
4332 except==[]? _normalize_edges(sum([for (x=v) _edge_set(x)])) :
4333 _normalize_edges(
4334 _normalize_edges(sum([for (x=v) _edge_set(x)])) -
4335 sum([for (x=except) _edge_set(x)])
4336 );
4337
4338
4339/// Internal Module: _show_edges()
4340/// Topics: Edges, Debugging
4341/// Usage:
4342/// _show_edges(edges, [size=], [text=], [txtsize=]);
4343/// Description:
4344/// Draws a semi-transparent cube with the given edges highlighted in red.
4345/// Arguments:
4346/// edges = The edges to highlight.
4347/// size = The scalar size of the cube.
4348/// text = The text to show on the front of the cube.
4349/// txtsize = The size of the text.
4350/// See Also: _edges(), EDGES_NONE, EDGES_ALL
4351/// Example:
4352/// _show_edges(size=30, edges=["X","Y"]);
4353module _show_edges(edges="ALL", size=20, text, txtsize=3,toplabel) {
4354 edge_set = _edges(edges);
4355 text = !is_undef(text) ? text : _edges_text(edges);
4356 color("red") {
4357 for (axis=[0:2], i=[0:3]) {
4358 if (edge_set[axis][i] > 0) {
4359 translate(EDGE_OFFSETS[axis][i]*size/2) {
4360 if (axis==0) xcyl(h=size, d=2);
4361 if (axis==1) ycyl(h=size, d=2);
4362 if (axis==2) zcyl(h=size, d=2);
4363 }
4364 }
4365 }
4366 }
4367 fwd(size/2) _edges_text3d(text, size=txtsize);
4368 color("yellow",0.7) cuboid(size=size);
4369 vpr = [55,0,25];
4370 color("black")
4371 if (is_def(toplabel))
4372 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=0.1,orient=UP,anchor=FRONT);
4373}
4374
4375
4376
4377
4378/// Internal Constant: CORNERS_NONE
4379/// Topics: Corners
4380/// Description:
4381/// The set of no corners.
4382/// Figure(3D):
4383/// _show_corners(corners="NONE");
4384/// See Also: CORNERS_ALL, corners()
4385CORNERS_NONE = [0,0,0,0,0,0,0,0]; // No corners.
4386
4387
4388/// Internal Constant: CORNERS_ALL
4389/// Topics: Corners
4390/// Description:
4391/// The set of all corners.
4392/// Figure(3D):
4393/// _show_corners(corners="ALL");
4394/// See Also: CORNERS_NONE, _corners()
4395CORNERS_ALL = [1,1,1,1,1,1,1,1];
4396
4397
4398/// Internal Constant: CORNER_OFFSETS
4399/// Topics: Corners
4400/// Description:
4401/// The vectors pointing to each corner of a unit sized cube.
4402/// Each item in a corner array will have a corresponding vector in this array.
4403/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4404CORNER_OFFSETS = [
4405 [-1,-1,-1], [ 1,-1,-1], [-1, 1,-1], [ 1, 1,-1],
4406 [-1,-1, 1], [ 1,-1, 1], [-1, 1, 1], [ 1, 1, 1]
4407];
4408
4409
4410
4411
4412/// Internal Function: _is_corner_array()
4413/// Topics: Corners, Type Checking
4414/// Usage:
4415/// bool = _is_corner_array(x)
4416/// Description:
4417/// Returns true if the given value has the form of a corner array.
4418/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4419function _is_corner_array(x) = is_vector(x) && len(x)==8 && all([for (xx=x) xx==1||xx==0]);
4420
4421
4422/// Internal Function: _normalize_corners()
4423/// Topics: Corners
4424/// Usage:
4425/// corns = _normalize_corners(v);
4426/// Description:
4427/// Normalizes all values in a corner array to be `1`, if it was originally greater than `0`,
4428/// or `0`, if it was originally less than or equal to `0`.
4429/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4430function _normalize_corners(v) = [for (x=v) x>0? 1 : 0];
4431
4432
4433function _corner_set(v) =
4434 _is_corner_array(v)? v : [
4435 for (i=[0:7]) let(
4436 v2 = CORNER_OFFSETS[i]
4437 ) (
4438 is_string(v)? (
4439 v=="ALL"? true : // Return all corners.
4440 v=="NONE"? false : // Return no corners.
4441 let(valid_values = ["ALL", "NONE"])
4442 assert(
4443 in_list(v, valid_values),
4444 str(v, " must be a vector, corner array, or one of ", valid_values)
4445 ) v
4446 ) :
4447 all([for (i=[0:2]) !v[i] || (v[i]==v2[i])])
4448 )? 1 : 0
4449];
4450
4451
4452/// Function: _corners()
4453/// Topics: Corners
4454/// Usage:
4455/// corns = _corners(v);
4456/// corns = _corners(v, except);
4457/// Description:
4458/// Takes a list of corner set descriptors, and returns a normalized corners array
4459/// that represents all those given corners. If the `except` argument is given
4460/// a list of corner set descriptors, then all those corners will be removed
4461/// from the returned corners array. If either argument only has a single corner
4462/// set descriptor, you do not have to pass it in a list.
4463function _corners(v, except=[]) =
4464 v==[] ? CORNERS_NONE :
4465 (is_string(v) || is_vector(v) || _is_corner_array(v))? _corners([v], except=except) :
4466 (is_string(except) || is_vector(except) || _is_corner_array(except))? _corners(v, except=[except]) :
4467 except==[]? _normalize_corners(sum([for (x=v) _corner_set(x)])) :
4468 let(
4469 a = _normalize_corners(sum([for (x=v) _corner_set(x)])),
4470 b = _normalize_corners(sum([for (x=except) _corner_set(x)]))
4471 ) _normalize_corners(a - b);
4472
4473
4474/// Internal Function: _corner_edges()
4475/// Topics: Corners
4476/// Description:
4477/// Returns [XCOUNT,YCOUNT,ZCOUNT] where each is the count of edges aligned with that
4478/// axis that are in the edge set and touch the given corner.
4479/// Arguments:
4480/// edges = Standard edges array.
4481/// v = Vector pointing to the corner to count edge intersections at.
4482/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4483function _corner_edges(edges, v) =
4484 let(u = (v+[1,1,1])/2) [edges[0][u.y+u.z*2], edges[1][u.x+u.z*2], edges[2][u.x+u.y*2]];
4485
4486
4487/// InternalFunction: _corner_edge_count()
4488/// Topics: Corners
4489/// Description:
4490/// Counts how many given edges intersect at a specific corner.
4491/// Arguments:
4492/// edges = Standard edges array.
4493/// v = Vector pointing to the corner to count edge intersections at.
4494/// See Also: CORNERS_NONE, CORNERS_ALL, _corners()
4495function _corner_edge_count(edges, v) =
4496 let(u = (v+[1,1,1])/2) edges[0][u.y+u.z*2] + edges[1][u.x+u.z*2] + edges[2][u.x+u.y*2];
4497
4498
4499function _corners_text(corners) =
4500 is_string(corners) ? [str("\"",corners,"\"")] :
4501 corners==CORNERS_NONE ? ["CORNERS_NONE"] :
4502 corners==CORNERS_ALL ? ["CORNERS_ALL"] :
4503 _is_corner_array(corners) ? [""] :
4504 is_vector(corners,3) ? _edges_vec_txt(corners) :
4505 is_list(corners) ? let(
4506 lst = [for (x=corners) each _corners_text(x)],
4507 out = [
4508 for (i=idx(lst))
4509 str(
4510 (i==0? "[" : ""),
4511 lst[i],
4512 (i<len(lst)-1? "," : ""),
4513 (i==len(lst)-1? "]" : "")
4514 )
4515 ]
4516 ) out :
4517 [""];
4518
4519
4520/// Internal Module: _show_corners()
4521/// Topics: Corners, Debugging
4522/// Usage:
4523/// _show_corners(corners, [size=], [text=], [txtsize=]);
4524/// Description:
4525/// Draws a semi-transparent cube with the given corners highlighted in red.
4526/// Arguments:
4527/// corners = The corners to highlight.
4528/// size = The scalar size of the cube.
4529/// text = If given, overrides the text to be shown on the front of the cube.
4530/// txtsize = The size of the text.
4531/// See Also: CORNERS_NONE, CORNERS_ALL, corners()
4532/// Example:
4533/// _show_corners(corners=FWD+RIGHT, size=30);
4534module _show_corners(corners="ALL", size=20, text, txtsize=3,toplabel) {
4535 corner_set = _corners(corners);
4536 text = !is_undef(text) ? text : _corners_text(corners);
4537 for (i=[0:7]) if (corner_set[i]>0)
4538 translate(CORNER_OFFSETS[i]*size/2)
4539 color("red") sphere(d=2, $fn=16);
4540 fwd(size/2) _edges_text3d(text, size=txtsize);
4541 color("yellow",0.7) cuboid(size=size);
4542 vpr = [55,0,25];
4543 color("black")
4544 if (is_def(toplabel))
4545 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=.1,orient=UP,anchor=FRONT);
4546}
4547
4548module _show_cube_faces(faces, size=20, toplabel,botlabel) {
4549 color("red")
4550 for(f=faces){
4551 move(f*size/2) rot(from=UP,to=f)
4552 cuboid([size,size,.1]);
4553 }
4554 vpr = [55,0,25];
4555 color("black"){
4556 if (is_def(toplabel))
4557 for(h=idx(toplabel)) up(21+6*h)rot(vpr) text3d(select(toplabel,-h-1),size=3.3,h=.1,orient=UP,anchor=FRONT);
4558 if (is_def(botlabel))
4559 for(h=idx(botlabel)) down(26+6*h)rot(vpr) text3d(botlabel[h],size=3.3,h=.1,orient=UP,anchor=FRONT);
4560 }
4561 color("yellow",0.7) cuboid(size=size);
4562}
4563
4564// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap